HDU STEP 5.1 并查集
5.1.1 HDU1829 A Bug's Life
给出异性对a,b 判断是否有冲突,即给出a,b异性,a,c异性,又给出b,c异性,显然是冲突了(同性恋~)
用opp[x]数组表示与x对立的bug,(其实就是判断一个二分图)
输入a,b后显然分四种情况
1,opp[a]==-1,opp[b]==-1,a和b都没有出现过,分别加到各自的对立集合 opp[a]=b opp[b]=a
2,opp[a]!=-1,opp[b]==-1,b没有出现过,将其合并到a的对立集合里,opp[b]=a,merge(b,opp[a])
3.opp[a]==1,opp[b]!=-1同上处理
4.opp[a]!=-1,opp[b]!=-1,a和b都出现过,分别合并到各自的对立集合,merge(opp[a],b),merge(opp[b],a)
#include <cstdio> #include <cstdlib> using namespace std; const int MAXN=2010; //opp保存与a异性的bug int cas,n,m,a,b,yes,p[MAXN],opp[MAXN]; char c; void init(){ for(int i=1;i<=n;i++)p[i]=i,opp[i]=-1; yes=1; } int find(int x){ if(x!=p[x])p[x]=find(p[x]); return p[x]; } void merge(int x,int y){ p[find(x)]=find(y); } inline void scan(int &x){ while(c=getchar(),c<'0'||c>'9');x=c-'0'; while(c=getchar(),c>='0'&&c<='9')x=x*10+c-'0'; } int main(){ scan(cas); for(int i=1;i<=cas;i++){ scan(n);scan(m); init(); while(m--){ scan(a);scan(b); if(yes){ //都没有出现过 if(opp[a]==-1&&opp[b]==-1){ opp[a]=b,opp[b]=a; //出现过其中一个,将另一个与它的异性集合合并 }else if(opp[a]==-1){ opp[a]=b; merge(a,opp[b]); }else if(opp[b]==-1){ opp[b]=a; merge(b,opp[a]); //都出现过,先查找是否在一个集合中,若不在,和对方的异性集合合并 }else{ if(find(a)==find(b))yes=0; else{ merge(a,opp[b]); merge(b,opp[a]); } } } } printf("Scenario #%d:\n",i); printf(yes?"No suspicious bugs found!\n":"Suspicious bugs found!\n"); printf("\n"); } return 0; }
这题写了我很久..只能说我对树的理解还不够深刻..
三个条件,不能有环,不能是森林,每个点入度最大为1(一直没注意这种情况!!)
前两个用并查集都可以判断,入度用一个数组在读取的时候保存
#include <cstdio> #include <string.h> using namespace std; const int MAXN=100005; int par[MAXN],yes,hash[MAXN],in[MAXN]; void init(){ yes=1; memset(hash,0,sizeof hash); memset(in,0,sizeof in); for(int i=1;i<MAXN;i++)par[i]=i; } int find(int x){ if(x!=par[x])par[x]=find(par[x]); return par[x]; } void merge(int x,int y){ par[find(x)]=find(y); } int main(){ int a,b,cas=1,ta; init(); while(scanf("%d%d",&a,&b)){ if(a<0&&b<0)break; if(a==0&&b==0){ if(yes){ for(int i=1;i<MAXN;i++){if(hash[i]){ta=find(i);break;}} for(int i=1;i<MAXN;i++){ //不能有点的入度为2 if(in[i]>1){yes=0;break;} //不能是森林 if(hash[i]&&ta!=find(i)){yes=0;break;} } } if(yes)printf("Case %d is a tree.\n",cas++); else printf("Case %d is not a tree.\n",cas++); init(); }else{ //不能生成环 if(find(a)==find(b))yes=0; if(!yes)continue; hash[a]=hash[b]=1; in[b]++; merge(a,b); } } }
找一条路使路上的最大值和最小值之差最小
并查集可以判连通,但是没有想到这题也可以用并查集做..
将边的权值从小到大排序,然后开始枚举,依次添加比它大的边,知道起点和终点连通为止(find(u)==find(v)),取最小值作为结果
#include <cstdio> #include <cmath> #include <algorithm> using namespace std; struct sars{ int u,v,w; bool operator <(const sars& s)const{return w<s.w;} }s[1001]; int n,m,pa[201]; void init(){for(int i=0;i<201;i++)pa[i]=i;} int find(int x){return x==pa[x]?x:pa[x]=find(pa[x]);} void merge(int x,int y){x=find(x),y=find(y);if(x!=y)pa[x]=y;} int solve(int st,int en){ //判断是否连通 init(); for(int i=0;i<m;i++){ merge(s[i].u,s[i].v); } if(find(st)!=find(en))return -1; //以每条边为起始边,依次添加权值比它大的边,直到连通,连通边-起始边=差值 int res=1e9; for(int i=0;i<m;i++){ init(); for(int j=i;j<m;j++){ if(s[j].w-s[i].w>=res)continue; merge(s[j].u,s[j].v); if(find(st)==find(en))res=min(res,s[j].w-s[i].w); } } return res; } char c; inline void scan(int &x){ while(c=getchar(),c<'0'||c>'9');x=c-'0'; while(c=getchar(),c>='0'&&c<='9')x=x*10+c-'0'; } int main(){ while(scanf("%d%d",&n,&m)!=EOF){ int a,b,c; for(int i=0;i<m;i++){ scan(s[i].u);scan(s[i].v);scan(s[i].w); } //按权值排序 sort(s,s+m); scan(a); for(int i=0;i<a;i++){ scan(b);scan(c); printf("%d\n",solve(b,c)); } } return 0; }
5.1.4 HDU3461 Code Lock
一道好题,不过思路比较难想
如果没有区间存在,答案是26^n,每增加一个区间,n-1(这里试一下就知道了,证明也很容易,因为这个区间可以变成26种状态~).但是要注意的是,比如已经有(1,10)和(1,3)在了,此时再增加(4,10)就没有作用了..
使用并查集对于[l,r]我们将l,r+1两个点并起,如果新线段的两个点是同一个集合,就不用减了
#include <cstdio> using namespace std; const int MAXN=10000010; const int PMOD=1000000007; int n,m,a,b,ans; int p[MAXN]; void init(){for(int i=1;i<=n+2;i++)p[i]=i;} int find(int x){return x==p[x]?x:p[x]=find(p[x]);} int merge(int x,int y){ x=find(x),y=find(y); if(x==y)return 0; p[x]=y; return 1; } long long mi(int x){//二分求求26^x if(x==0)return 1; long long a=mi(x/2); a=a*a%PMOD; if(x%2==1)a=a*26%PMOD; return a; } int main(){ while(scanf("%d%d",&n,&m)!=EOF){ init(); ans=0; while(m--){ /* 每加一个线段就会x1 不过注意线段划分时会出现重复比如 [1,10],[1,3] 这时候再来一个[4,10]就不用减了,因为前两个线段已经划出这个了 使用并查集对于[l,r]我们将l,r+1两个点并起 如果新线段的两个点是同一个集合,就不用减了 */ scanf("%d%d",&a,&b); ans+=merge(a,b+1); } printf("%I64d\n",mi(n-ans)); } }
很裸的并查集,用负节点标记集合元素个数,ts[x]代表x运送次数,在压缩路径很修改
#include <cstdio> #include <cstdlib> using namespace std; const int MAXN=11000; int cas,n,a,b,q,p[MAXN],ts[MAXN]; char c[3],cc; void init(){ for(int i=1;i<=n;i++){p[i]=-1;ts[i]=0;} } int find(int x){ if(p[x]<0)return x; int t=p[x]; p[x]=find(p[x]); ts[x]+=ts[t]; return p[x]; } void merge(int x,int y){ x=find(x),y=find(y); if(x!=y)p[y]+=p[x],p[x]=y,ts[x]=1; } inline void scan(int &x){ while(cc=getchar(),cc<'0'||cc>'9');x=cc-'0'; while(cc=getchar(),cc>='0'&&cc<='9')x=x*10+cc-'0'; } int main(){ scan(cas); for(int ca=1;ca<=cas;ca++){ printf("Case %d:\n",ca); scan(n);scan(q); init(); while(q--){ scanf("%s",c); if(c[0]=='T'){ scan(a);scan(b); merge(a,b); }else{ scan(a); int tt=find(a); printf("%d %d %d\n",tt,-p[tt],ts[a]); } } } //system("pause"); return 0; }
好题啊..虚拟根节点,这样在删除时就不会影响到其它的节点了.
#include <cstdio> #include <string.h> using namespace std; int n,m,a,b,cnt,p[2000000],s[1000041],ans; char c; inline void scan(int &x){ while(c=getchar(),c<'0'||c>'9');x=c-'0'; while(c=getchar(),c>='0'&&c<='9')x=x*10+c-'0'; } void init(){ cnt=n*2,ans=0; for(int i=0;i<n;i++)p[i]=i+n; for(int i=n;i<n*2+m;i++)p[i]=i; memset(s,0,sizeof s); } int find(int x){return x==p[x]?x:p[x]=find(p[x]);} void merge(int x,int y){p[find(x)]=find(y);} //固定根节点,n+1~n+n作为根节点,而1~n作为虚拟根节点(指向n+1~n+n),之后在增添n+n~n+n+m作为备用节点 //删除时直接修改1~n指向的节点到n+n后的节点 int main(){ int cas=1; while(scanf("%d%d",&n,&m),n||m){ init(); for(int mm=0;mm<m;mm++){ scanf(" %c",&c); if(c=='M'){ scan(a);scan(b); merge(a,b); }else{ scan(a); p[a]=cnt++; } } for(int i=0;i<n;i++){if(s[find(i)]==0){ans++,s[find(i)]=1;}} printf("Case #%d: %d\n",cas++,ans); } return 0; }
裸并查集,给字符串写个哈希函数就可以了
5.1.8 HDU3038 How Many Answers Are Wrong
压根没想到怎么用并查集做..还是看了大牛的解题报告
和上面那一题Code Lock有点像,[1,10]和[1,4]确定了,[5,10]也就确定了
所以将每次端点合并,sum[x]表示tot[x]-tot[root](即第x+1个数到第root个数的和) tot[x]表示前x个数的和
#include <cstdio> using namespace std; const int MAXN=200010; int n,m,ai,bi,si,wa; int p[MAXN],sum[MAXN]; void init(){for(int i=0;i<=n;i++)p[i]=i,sum[i]=0;} int find(int x){ if(x!=p[x]){ int t=p[x]; p[x]=find(p[x]); sum[x]+=sum[t]; } return p[x]; } bool merge(int x,int y,int c){ int px=find(x),py=find(y); if(px==py)return sum[x]-sum[y]==c; //sum[x]表示tot[r]-tot[x],r=root; p[px]=py; sum[px]=sum[y]-sum[x]+c; return true; } int main(){ while(~scanf("%d%d",&n,&m)){ wa=0; init(); while(m--){ scanf("%d%d%d",&ai,&bi,&si); if(!merge(ai-1,bi,si))wa++; } printf("%d\n",wa); } return 0; }