题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=6763
题目大意:有一个n个点m条边的无向图,结点从1开始编号到n,每个结点都有一个权值bi,现在希望可以将所有结点的权值改为0
可以进行下面的操作:
1:选择一个正整数k(1<=k<=n)
2:选择k个不同的结点c1,c2....ck(1<=ck<=n),并且这k个结点可以相互连通,也就是这k个点是连通的
3:这k个城市每个城市的权值都减1
问最小需要多少次这样的操作可以将所有结点的权值都变为0
输入:
第一行是一个整数t,表示样例数量
对于每个样例,第一行包含两个整数n与m(1<=n<=100000,1<=m<=200000)
第二行n个整数b1,b2......bn表示每个结点的权值
接下来m行,每行两个整数u,v(1<=u,v<=n,u!=v),表示结点u与结点v时间有边
输出:
对于每个样例,输出一个整数表示需要进行的最小的操作数
题解:对于这道题,第一次我的想法是,从结点1开始进行遍历,每次遍历通过bfs找到该结点可以到达的所有结点,并找出这些结点与当前结点的权值的最小值,
将这些结点的权值都减去这个最小值,并且最后的答案ans+=这个最小值。只有当该结点的权值为0,才会对下一个点进行遍历,因为每次遍历必定会最少有一个
结点的权值变为0.所以最多也只是会进行n次遍历,但是每次遍历的最坏情况是O(n),所以最后的复杂度是O(n2),显然对于这道题我不出意外的超时了
最后题解的正确打开方式是总体思路也是与我之前的类似,不过这里出现了一个不同,就是不再是正面求解,而是从反方向进行考虑,不再看每个集合中权值的
最小值,而是将权值进行排序 ,从大到小进行排序,并且从大到小进行遍历,依次将所有结点加入到图中去。每次遍历是查找与该结点u直接相连的所有vi,如果vi
之前已经遍历完(或者说vi之前已经放入图中),则进行下面的操作,否则不进行任何操作。操作,将vi所在的并查集(这里需要用到并查集的只是,也就是需要
将放入图中的点使用并查集来判断那些点之间是连通的)的祖先节点x的父亲改为u(也就是当前遍历的这个结点),并且将原先的祖先结点x所对应的权值减去当前结点
u的权值,并将这个差加到最后的答案ans中。当所有的点都遍历完毕之后,然后在将n个点中是祖先结点的权值加到答案ans中。此时ans就是最后的答案。
这里的b[x]-b[u]其实是相对于x而言所贡献的操作次数,对于最后的祖先结点,其权值就是其最后的贡献的操作次数。
反思:首先对于第一种超时的方法,哪里的操作是无效的或者那些操作是重复的或者说操作是可以一步到位的,而我却是分布步做的。就是对于每个权值的相减
操作,我们可不可以对于每个结点的权值来说一步到位,也就是直接计算这个结点的实际贡献度,这便是第二个方法的主要思维。对于某些问题,自己有点不会
变通,有时正向操作比较麻烦,我们可以自己试试反向进行思考是否会出现可以令人惊喜的结果。
AC代码
#include<iostream> #include<cstdio> #include<algorithm> #include<vector> using namespace std; const int N=100000+5; const int M=200000+5; int n,m,u,v; struct node{ int b,pos; }pb[N]; int now[N]; vector<int>E[N]; int fa[N],p[N]; long long int s=0; int cmp(struct node pb1,struct node pb2){ return pb1.b>pb2.b; } int find(int x){//并查集查找祖宗节点同时进行路径压缩 if(fa[x]==x) return x; return fa[x]=find(fa[x]); } int add(int x,int y){ int ry=find(y);//找到需要操作的集合的祖宗节点 s+=now[ry]-now[x];//贡献当前节点的操作权值 fa[ry]=x;//更改祖宗节点 } int main(){ int t; cin>>t; while(t--){ s=0; scanf("%d%d",&n,&m); //输入数据、初始化标志数组、初始化并查集数组 //数据结构pb.i存储下标,pb.b存储该下标对应的权值b[i] //now[i]存储下标i对应的权值,因为上面的pb需要进行排序,并不能通过数组pb的下标i找到对应的b //,只能循环遍历查找,这样复杂度就会比较高 for(int i=1;i<=n;i++) scanf("%d",&pb[i].b),pb[i].pos=i,p[i]=0,fa[i]=i,now[i]=pb[i].b; while(m--){ scanf("%d%d",&u,&v); E[u].push_back(v);//使用不定长数组存储图 E[v].push_back(u); } sort(pb+1,pb+n+1,cmp);//按照权值b从大到小进行排序 for(int i=1;i<=n;i++){//对排序好的数组,从最大的权值开始进行操作 u=pb[i].pos;//找到未访问的最大权值的所对应的下标 int size=E[u].size();//该下标所对应的节点的边数 p[u]=1;//对于放入图中的点进行标记 for(int j=0;j<size;j++){ if(p[E[u][j]]==0) continue;//只有当与当前节点所连的节点已经放入图中时,才会进行下一次操作 //若是跟u相连的其他节点已经放入图中,并且还是在一个集合中,则不需要再此进行操作 //其实不需要这个判定条件也可以,因为即使其他所连节点在一个集合中也没有关系 //因为当第二次执行这个函数时(第二次对之前已经做过的集合进行操作时),之前的集合的祖宗节点已经是u //所以对于s而言是相当于加了一个0,对于最后的结果并没有什么影响 if(find(u)!=find(E[u][j])) add(u,E[u][j]);//将u放如途中所需要进行的函数操作 } } for(int i=1;i<=n;i++) if(fa[i]==i){ s+=now[i];//最后还需要将所有的祖宗节点的权值加入进s } cout<<s<<endl; for(int i=1;i<=n;i++) E[i].clear();//这一步要注意,之前没有这一步显示栈溢出 } return 0; }