二分图
匈牙利算法
bzoj1854 游戏
题目大意:给定n个武器,每个武器有两个属性值,每个武器用一次,求最长连续递增序列。
思路:如果把属性值看作一排点,武器看作一排点,就是二分图最大匹配了。在匈牙利中有个visit数组,如果每次赋值的话,会tle,这里有一种很好的做法,就是把visit改成int,每次和这一次的循环变量比一下就行了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 1000005 #define up 10000 using namespace std; int point[maxnode]={0},next[maxnode*2]={0},en[maxnode*2]={0},tot=0, match[maxnode]={0},visit[maxnode]={0}; void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} bool find(int x){ int i,j; for (j=point[x];j;j=next[j]){ if (visit[en[j]]!=tot){ visit[en[j]]=tot; if (!match[en[j]]||find(match[en[j]])){ match[en[j]]=x;return true; } } }return false; } int main() { int i,j,x,y,n;scanf("%d",&n); for (i=1;i<=n;++i){ scanf("%d%d",&x,&y); add(x,i);add(y,i); } for (i=1;i<=up;++i){tot=i;if (!find(i)) break;} printf("%d\n",i-1); }
bzoj1059 矩阵游戏
题目大意:给定一个n*n的01矩阵,判断能否通过整行或者整列交换,使得左上到右下的对角线上都是1。
思路:对于格点的二分图问题之前见到过,不过这道题还是在sunshine大爷说出算法之后才恍然大悟的。对于1的点从i到j连边,如果二分图能够完美匹配,就是Yes,否则No。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 40005 using namespace std; int point[maxm],next[maxm],en[maxm],match[maxm],visit[maxm],tot; void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} bool find(int u){ int v,i,j; for (i=point[u];i;i=next[i]) if (visit[j=en[i]]!=tot){ visit[j]=tot; if (!match[j]||find(match[j])){ match[j]=u;return true; } } return false; } int main(){ int n,m,t,i,j;scanf("%d",&t); while(t--){ memset(point,0,sizeof(point)); memset(next,0,sizeof(next)); memset(visit,0,sizeof(visit)); memset(match,0,sizeof(match)); scanf("%d",&n);tot=0; for (i=1;i<=n;++i) for (j=1;j<=n;++j){scanf("%d",&m);if(m) add(i,j);} for (tot=1;tot<=n;++tot) if(!find(tot)) break; if (tot<=n) printf("No\n"); else printf("Yes\n"); } }
cogs746 骑士共存
题目大意:在n*n的棋盘上放马,要求互不攻击,求最多能放多少个。
思路:为了构建二分图,我们把棋盘黑白染色,黑色格子只能向白色连边(马跳跃的性质),然后找最大匹配,用总点数(无障碍的)-最大匹配数就是答案。
注意:二分图,把棋盘分成两部分的思想。(这就是二分图最大独立子集问题,证明比较简单,我们可以看做删掉最少的点,使剩下的点之间没有连边,即用最小覆盖的答案,而最小覆盖又等于最大匹配,所以就可以表示成总点数-最大匹配数了。)
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int db[201][201],match[40001]={0},next[700000]={0},point[40001]={0},en[700000]={0},tot=0, dx[8]={-2,-2,-1,-1,1,1,2,2},dy[8]={-1,1,-2,2,-2,2,-1,1}; bool visit[40001]={false}; void add(int st,int enn) { ++tot;next[tot]=point[st];point[st]=tot;en[tot]=enn; ++tot;next[tot]=point[enn];point[enn]=tot;en[tot]=st; } bool find(int x) { int i; for (i=point[x];i!=0;i=next[i]) { if (!visit[en[i]]) { visit[en[i]]=true; if (!match[en[i]]||find(match[en[i]])) { match[en[i]]=x;return true; } } } return false; } int main() { int i,j,n,m,totd=0,x,y,t,ans=0; scanf("%d%d",&n,&m); for (i=1;i<=n;++i) for (j=1;j<=n;++j) db[i][j]=++totd; for (i=1;i<=m;++i) { scanf("%d%d",&x,&y); db[x][y]=0; } for (i=1;i<=n;++i) for (j=1;j<=n;++j) { if (db[i][j]&&((i+j)%2==0)) { for (t=0;t<8;++t) { x=i+dx[t];y=j+dy[t]; if (x<1||x>n) continue; if (y<1||y>n) continue; if (!db[x][y]) continue; add(db[i][j],db[x][y]); } } } for (i=1;i<=n;++i) for (j=1;j<=n;++j) { if (!db[i][j]||(i+j)%2==1) continue; memset(visit,false,sizeof(visit)); if (find(db[i][j])) ++ans; } ans=totd-ans-m; printf("%d\n",ans); }
bzoj3175 攻击装置
题目大意:在一个有障碍的网格中放马,求互不攻击最多的个数。
思路:同上,二分图最大独立子集。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 205 #define maxe 1000000 using namespace std; int dx[8]={-1,-2,1,2,-1,-2,1,2},dy[8]={-2,-1,-2,-1,2,1,2,1},bi[maxm][maxm]={0},cnt=0, point[maxe]={0},next[maxe]={0},en[maxe]={0},tot=0,visit[maxm*maxm]={0}, match[maxm*maxm]={0},map[maxm][maxm]={0}; char in(){ char ch; while(scanf("%c",&ch)==1) if (ch>='0'&&ch<='1') return ch; } void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} bool find(int u){ int i,j,v; for (i=point[u];i;i=next[i]){ if (visit[v=en[i]]==tot) continue; visit[v]=tot; if (!match[v]||find(match[v])){ match[v]=u;return true; } }return false; } int main(){ int n,i,j,k,x,y,ans=0;char ch; scanf("%d",&n); for (i=1;i<=n;++i) for (j=1;j<=n;++j){ ch=in();map[i][j]=ch-'0'; if (!map[i][j]) bi[i][j]=++cnt; }for (i=1;i<=n;++i) for (j=1;j<=n;++j){ if (map[i][j]) continue; for(k=0;k<8;++k){ x=i+dx[k];y=j+dy[k]; if (x<1||x>n||y<1||y>n||map[x][y]) continue; add(bi[i][j],bi[x][y]); } }for (tot=0,i=1;i<=n;++i) for (j=1;j<=n;++j){ if (map[i][j]||((i+j)%2)) continue; ++tot;if (find(bi[i][j])) ++ans; }printf("%d\n",cnt-ans); }
bzoj1562 序列变换
题目大意:定义dis(i,j)=min(abs(i-j),n-abs(i-j)),求一个序列0~n-1的置换Ti,使得dis(i,Ti)为给定值。
思路:每个点一定对应着两个点,所以可以二分图跑一下,对于输出方案的字典序最小,又变成了贪心的时候对应位置的字典序最大,那么就是二分图的时候先选终点小的,从大的点开始跑的话,如果要更改的话,大的点只会变大,所以就是字典序最小了(其实做的时候是把这些顺序换一下,硬凑出来的。)(还有就是一定要考虑终点的大小,因为%了之后+不一定大、-不一定小)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 20005 using namespace std; int point[maxm]={0},en[maxm],next[maxm]={0},match[maxm]={0},visit[maxm]={0},tot=0,ans[maxm]; void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} bool judge(int u,int t){ int i,j,v; for (i=point[u];i;i=next[i]){ if (visit[v=en[i]]==t) continue; visit[v]=t; if (!match[v]||judge(match[v],t)){ match[v]=u;return true; } }return false; } int main(){ int i,j,n,x,a,b;scanf("%d",&n); for (i=0;i<n;++i){ scanf("%d",&x); a=(i-x+n)%n;b=(i+x)%n; if (a<b) swap(a,b); add(i,a);add(i,b); }for (j=0,i=n-1;i>=0;--i) if (judge(i,i+1)) ++j; if (j<n) printf("No Answer\n"); else{ for (i=0;i<n;++i) ans[match[i]]=i; for (i=0;i<n-1;++i) printf("%d ",ans[i]); printf("%d\n",ans[n-1]); } }
bzoj4276 Bajtman i Okragly Robin
题目大意:已知n个强盗,每个强盗会在ai~bi某一秒时间里偷ci金币,可以在每一秒内阻止一个强盗偷金币,问最多能阻止多少钱。
思路:感觉像一个最大完美匹配,但是不会具体的算法,就用贪心+匈牙利做的。先按强盗偷的钱排降序,两排点是强盗和时间,相应的连边(n^2级别吧),然后匈牙利,能匹配就给答案加上这个值。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 10005 #define maxe 25000005 using namespace std; struct use{ int ai,bi,ci; }rob[maxm]; int point[maxm]={0},next[maxe]={0},en[maxe]={0},tot=0,match[maxm]={0},visit[maxm]={0}; int cmp(const use&x,const use&y){return x.ci>y.ci;} void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;} bool find(int u,int kk){ int i,j,v; for (i=point[u];i;i=next[i]) if (visit[v=en[i]]!=kk){ visit[v]=kk; if (!match[v]||find(match[v],kk)){ match[v]=u;return true; } }return false; } int main(){ int n,i,j,ans=0;scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d%d%d",&rob[i].ai,&rob[i].bi,&rob[i].ci); sort(rob+1,rob+n+1,cmp); for (i=1;i<=n;++i) for (j=rob[i].ai;j<rob[i].bi;++j) add(i,j); for (i=1;i<=n;++i) if (find(i,i)) ans+=rob[i].ci; printf("%d\n",ans); }
KM
bzoj1937 最小生成树(!!!)
题目大意:给定一个联通的无向图,有边权,其中有n-1条树边,现在希望改变边的权值,使树边一定出现在最小生成树中(可以有多棵最小生成树),求最小的修改的和。
思路:考虑对边的修改,设每条边改变的量是di(di>=0),非树边的权值一定是ci+di,树边的权值一定是ci-di,同时对于非树边i覆盖的树边j一定满足ci+di>=cj-dj,所以di+dj>=cj-ci,这个式子非常像km算法中lx+ly>=wxy,所以可以用km算法求解,对于边建点,树边i向能覆盖的非树边j连边cj-ci。复杂度是O(m^4)(实际效果非常好,可优化到O(m^3)),这样还可以求出修改的方案。
还有一种O(n^3)的方法,不能求方案。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 65 #define M 805 #define inf 2100000000 using namespace std; struct use{int x,y,c,k;}ed[M]; int w[M][M]={0},gi[M][M]={0},match[M],lx[M],ly[M],n,m,mi[M][M]={0}; bool si[M],ti[M]; int cmp(const use&x,const use&y){return x.k>y.k;} bool dfs(int u,int v,int fa,int id){ int i; if (u==v) return true; for (i=1;i<=n;++i){ if (i==fa||!gi[u][i]) continue; if (dfs(i,v,u,id)){ w[gi[u][i]][id]=ed[gi[u][i]].c-ed[id].c; return true; } }return false;} bool find(int u){ int i; si[u]=true; for (i=1;i<=m;++i) if (lx[u]+ly[i]==w[u][i]&&!ti[i]){ ti[i]=true; if (!match[i]||find(match[i])){ match[i]=u;return true; } }return false;} void updata(){ int mn,i,j;mn=inf; for (i=1;i<=m;++i) if (si[i]) for (j=1;j<=m;++j) if (!ti[j]) mn=min(mn,lx[i]+ly[j]-w[i][j]); for (i=1;i<=m;++i){ if (si[i]) lx[i]-=mn; if (ti[i]) ly[i]+=mn; } } void km(){ int i,j; for (i=1;i<=m;++i){ match[i]=lx[i]=ly[i]=0; for (j=1;j<=m;++j) lx[i]=max(lx[i],w[i][j]); }for (i=1;i<=m;++i) while(1){ for (j=1;j<=m;++j) si[j]=ti[j]=0; if (find(i)) break;else updata(); } } int main(){ int i,u,v,cc,ans=0; scanf("%d%d",&n,&m); for (i=1;i<=m;++i){ scanf("%d%d%d",&u,&v,&cc); ed[i]=(use){u,v,cc,0};mi[u][v]=mi[v][u]=i; }for (i=1;i<n;++i){ scanf("%d%d",&u,&v); ed[mi[u][v]].k=1; }sort(ed+1,ed+m+1,cmp); for (i=1;i<=m;++i){ if (ed[i].k) gi[ed[i].x][ed[i].y]=gi[ed[i].y][ed[i].x]=i; else dfs(ed[i].x,ed[i].y,0,i); }km(); for (i=1;i<n;++i) ans+=lx[i]; for (i=n;i<=m;++i) ans+=ly[i]; printf("%d\n",ans); }
bzoj3571 画框(!!!)
题目大意:1~n个元素1配对1~n个元素2,要求一一对应,并且已知每个元素1和元素2配对的权值ai,j、bi,j,求min sigma ai,pi * sigma bi,pi(pi表示i这个元素1对应的元素2)。
思路:分治+km。最小乘积生成树模型,将sigma aij和sigma bij看做横纵坐标,最优答案的乘积看做双曲线y=k/x的k,不断逼近最优曲线就可以了。先求点A表示sigma aij最小的,点B表示sigma bij最小的,这是一维的km;然后求离AB最远的且在AB下方的点C,设AB:ax+by+c=0,就是求最大的|a*C.x+b*C.y+c|/sqrt(a^2+b^2),a、b、c都是定值,所以就是求最大的a*ai,j+b*bi,j,这也是一维的km。考虑到C在AB下面,所以a*C.x+b*C.y+c>0,所以当a*C.x+b*C.y+c<=0或者A、B和C相等了就退出分治。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 75 #define inf 2100000000 using namespace std; struct use{ int x,y; bool operator==(const use&a){return x==a.x&&y==a.y;} }; int si[N]={0},ti[N]={0},ai[N][N],bi[N][N],wi[N][N],sla[N],lx[N],ly[N],n,tt=0,match[N],ans; bool find(int u){ int i;si[u]=tt; for (i=1;i<=n;++i){ if (ti[i]==tt) continue; if (lx[u]+ly[i]==wi[u][i]){ ti[i]=tt; if (!match[i]||find(match[i])){ match[i]=u;return true; } }else sla[i]=min(sla[i],lx[u]+ly[i]-wi[u][i]); }return false; } void updata(){ int i,mn;mn=inf; for (i=1;i<=n;++i) if (ti[i]!=tt) mn=min(mn,sla[i]); for (i=1;i<=n;++i){ if (si[i]==tt) lx[i]-=mn; if (ti[i]==tt) ly[i]+=mn; else sla[i]-=mn; } } use km(){ int i,j;use ci; for (i=1;i<=n;++i){ lx[i]=ly[i]=match[i]=0; for (j=1;j<=n;++j) lx[i]=max(lx[i],wi[i][j]); }for (i=1;i<=n;++i){ memset(sla,127,sizeof(sla)); while(1){ ++tt; if (find(i)) break; else updata(); } }ci.x=ci.y=0; for (i=1;i<=n;++i){ ci.x+=ai[match[i]][i]; ci.y+=bi[match[i]][i]; }ans=min(ans,ci.x*ci.y); return ci; } void work(use l,use r){ int i,j,aa,bb,cc; aa=r.y-l.y;bb=l.x-r.x;cc=-(aa*l.x+bb*l.y); for (i=1;i<=n;++i) for (j=1;j<=n;++j) wi[i][j]=aa*ai[i][j]+bb*bi[i][j]; use mid=km(); if (l==mid||r==mid) return; work(l,mid);work(mid,r); } int main(){ int i,j,t;use l,r;scanf("%d",&t); while(t--){ scanf("%d",&n);ans=inf; for (i=1;i<=n;++i) for (j=1;j<=n;++j) scanf("%d",&ai[i][j]); for (i=1;i<=n;++i) for (j=1;j<=n;++j) scanf("%d",&bi[i][j]); for (i=1;i<=n;++i) for (j=1;j<=n;++j) wi[i][j]=-ai[i][j]; l=km(); for (i=1;i<=n;++i) for (j=1;j<=n;++j) wi[i][j]=-bi[i][j]; r=km();work(l,r); printf("%d\n",ans); } }
其他
poj1112 Team Them Up!
题目大意:已知一个有向图,分成大小尽量接近的两部分,保证每部分中的点都互相直接连通。
思路:对于不能在一个部分的点连边,如果是二分图就是有解(!!!)。因为这样可能有很多个二分图,所以可以背包dp一下求尽量接近,记录转移,输出方案。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 105 using namespace std; int co[N]={0},n,m,va[N][2],cc[3]={0},id[N][3][N],gi[N][N]; bool mp[N][N],ed[N][N],fi[N][N]={false}; bool dfs(int u,int c){ int i; if (co[u]) return (co[u]==c); co[u]=c;id[m][c][++cc[c]]=u; for (i=1;i<=n;++i) if (ed[u][i]) if (!dfs(i,3-c)) return false; return true; } int main(){ int v,i,j,k,nn,nt;scanf("%d",&n); memset(mp,false,sizeof(mp)); memset(ed,false,sizeof(ed)); for (i=1;i<=n;++i) for (scanf("%d",&v);v;scanf("%d",&v)) mp[i][v]=true; for (i=1;i<=n;++i) for (j=1;j<=n;++j) if (i!=j&&(!mp[i][j]||!mp[j][i])) ed[i][j]=true; for (i=1;i<=n;++i) if (!co[i]){ cc[1]=cc[2]=0; ++m; if (!dfs(i,1)) break; va[m][0]=cc[1]; va[m][1]=cc[2]; } if (i<=n) printf("No solution\n"); else{ fi[0][0]=true; nn=n/2; for (i=1;i<=m;++i) for (k=0;k<2;++k) for (j=0;j+va[i][k]<=nn;++j) if (fi[i-1][j]){ fi[i][j+va[i][k]]=true; gi[i][j+va[i][k]]=k; } for (i=nn;i;--i) if (fi[m][i]) break; nt=i; memset(co,0,sizeof(co)); for (i=m;i;--i){ for (j=1;j<=va[i][gi[i][nt]];++j) co[id[i][gi[i][nt]+1][j]]=1; nt-=va[i][gi[i][nt]]; }cc[0]=cc[1]=0; for (i=1;i<=n;++i) ++cc[co[i]]; for (i=0;i<2;++i){ printf("%d",cc[i]); for (j=1;j<=n;++j) if (co[j]==i) printf(" %d",j); printf("\n"); } } }