[SCOI2010]连续攻击游戏

解题报告——连续攻击游戏

题干

lxhgww最近迷上了一款游戏,在游戏里,他拥有很多的装备,每种装备都有2个属性,这些属性的值用[1,10000]之间的数表示。当他使用某种装备时,他只能使用该装备的某一个属性。并且每种装备最多只能使用一次。 游戏进行到最后,lxhgww遇到了终极boss,这个终极boss很奇怪,攻击他的装备所使用的属性值必须从1开始连续递增地攻击,才能对boss产生伤害。也就是说一开始的时候,lxhgww只能使用某个属性值为1的装备攻击boss,然后只能使用某个属性值为2的装备攻击boss,然后只能使用某个属性值为3的装备攻击boss……以此类推。 现在lxhgww想知道他最多能连续攻击boss多少次?

Input

输入的第一行是一个整数N,表示lxhgww拥有N种装备 接下来N行,是对这N种装备的描述,每行2个数字,表示第i种装备的2个属性值

Output

输出一行,包括1个数字,表示lxhgww最多能连续攻击的次数。

Sample

in:3 1 2 3 2 4 5

out:2

Hint

对于30%的数据,保证N < =1000
对于100%的数据,保证N < =1000000

整活

上来一看以为是个水题,上去一个暴力试试水,喜提40分。转念一想发现对于同一次攻击我们要做出选择,假如有两件装备,它们都能打出2的攻击,但一件还能打出3的攻击,另一件还能打出5的攻击,那我们优先用哪一件呢?留着攻击力大的那个吗?并不是,这不像咱们玩游戏,你攻击力再大,你根本用不上又有什么卵用?也不一定就留攻击力小的那个,道理还是一样的,有可能别的武器也能达到这个攻击力,但却达不到那个较高的攻击力。所以,由于这种不确定,我们并不能直接从属性值上做出抉择。

原始版的暴力代码(笑)放上来给大家乐呵乐呵

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#define ll long long
#define Dio ios::sync_with_stdio(0)
using namespace std;
const int maxn=1000000+1;
int n;
vector<int> belong[maxn];
struct node{
    bool if_used;
}tool[maxn];
int Check(){
    int i;
    for(i=1;i<=n;i++){
        if(belong[i].size()==0) return 0;
        int cnt=0;
        for(int j=0;j<belong[i].size();j++){
            int id=belong[i][j];
            //printf("i=%d id=%d if_used=%d
",i,id,tool[id].if_used);
            if(!tool[id].if_used){
                tool[id].if_used=1;
                cnt++;
                break;
            }
        }
        if(cnt==0) return i-1;
    }
    return n;
}
int main(){
    cin>>n;
    int l,r;
    for(int i=1;i<=n;i++){
        cin>>l>>r;
        tool[i].if_used=0;
        belong[l].push_back(i);
        belong[r].push_back(i);
    }
    cout<<Check()<<endl;
    return 0;
}

正解

关于这道题的正解有很多,包括但不限于并查集、贪心、二分图、DFS、BFS等等,这里说一个我觉得比较好理解的BFS(我才不承认是别的我不会呢)。

我们知道:一个装备两个属性中只有一个能起作用。如果我们把两个属性值当做两个点来连线,那么我们可以感性地想象这条线上有一只猫,这只猫要不然趴在一头,要不然趴在另一头,哪个值被覆盖代表着这个装备提供哪个属性值(我理解为伪双向边,看似双向,实则单向,第一次通过这条边时的方向决定了以后通过这条边的方向)。如果将属性值作为边的两端来这样建一张图,对于每一个连通图,因为每一只猫都能覆盖旁边的一个点,我们可以很轻松的想象到,如果说这个图中有m条边,n个点,那么一定可以有min(n,m)个点被覆盖。(奇妙的比喻转自洛谷 @Windows_XP,我jio得很形象)

对于一张连通图,边数一定不小于点数(除非他是一棵树),然后根据伪双向边的特点,我们可以确定图上所有的点都是可达的。若是一棵树,显然有一个点是无法取到的,根据贪心的思想,我们肯定想办法让最大的那个点取不到。通过对BFS时的点数和边数维护比较得出答案(直接看代码吧)。

而对于树中最大的点如何处理呢?每一次BFS都会遍历整个连通块,其他的BFS是够不到这个最大点的。所以最大的那个点无法被其他的BFS覆盖。那么我们将它的状态视为if_can[x]=1,即它绝对无法被覆盖。

没有与任何点相连的点,也可以看做一个树。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
#include<cstring>
#define ll long long
#define Dio ios::sync_with_stdio(0)
using namespace std;
const int maxn=1000000+1;
int n;
struct Edge{
    int to,next;
}edge[maxn<<1];
int head[maxn],len=0;
void Add(int u,int v){
    edge[++len].to=v;
    edge[len].next=head[u];
    head[u]=len;
}
int vis[maxn],if_can[maxn];
bool Check(int u){
    if(vis[u]) return (!if_can[u]);
    vis[u]=1;
    int maxp=0,nump=0,nume=0;
    //分别记录该连通块内的最大点,总点数,总边数
    queue<int> q;
    q.push(u);
    while(!q.empty()){
        int x=q.front();
        q.pop();
        maxp=max(maxp,x);//更新最大点
        nump++;
        for(int i=head[x];i;i=edge[i].next){
            nume++;
            int v=edge[i].to;
            if(!vis[v]) vis[v]=1,q.push(v);
        }
    }
    nume>>=1;//因为是伪双向边,实际上真正的边数是经过边数的一半
    if(nume<nump) if_can[maxp]=1;//一个块内边数小于点数,显然一棵树,那么让最大的那个取不到
    return (!if_can[u]);
}
int main(){
    Dio;
    cin>>n;
    for(int i=1;i<=n;i++){
        int u,v;
        cin>>u>>v;
        Add(u,v),Add(v,u);
    }
    int k=1;//从1开始判断每个点是否可达
    while((vis[k]&&!if_can[k])||Check(k))
    //vis[k]和!if_cnt[k]的顺序不能换,只有vis[]过了这个点它的是否可达才是有意义的,后面的Check()就是为vis为0时准备的
        k++;
    cout<<k-1<<endl;
    return 0;
}
原文地址:https://www.cnblogs.com/Zfio/p/12822641.html