图论模板总结
为什么图论也有那么多代码??
一.最小生成树
1.最小生成树
prim
下面的写法是O(eloge)
有O(n^2)的写法:双重循环枚举每个点
typedef pair<int,int> pii;
priority_queue <pii,vector<pii>,greater<pii> >q;
void prim(){
memset(dis,inf,sizeof(dis));
dis[1]=0;q.push(make_pair(0,1));//!!
while(!q.empty()){
int u=q.top().second;q.pop();
if(vis[u]) continue;
vis[u]=1; ans+=dis[u];//!!
for(int i=head[u];i!=-1;i=e[i].nxt){
int v=e[i].v;
if(!vis[v]&&dis[v]>e[i].w){
dis[v]=e[i].w;
q.push(make_pair(dis[v],v));
}
}
}
}
kruskal
O(eloge)
struct edge{
int u,v,w;
bool operator < (const edge &b) const{
return w<b.w;
}
}e[M];
int Find(int x){return (fa[x]==x)?x:fa[x]=Find(fa[x]);}
void kruskal{
sort(e+1,e+m+1);
for(int i=1;i<=n;++i) fa[i]=i;
for(int i=1;i<=m;++i){
int x=Find(e[i].u),y=Find(e[i].v);
if(x!=y){
fa[x]=y;
ans+=e[i].w;
if(++cnt==n-1) break;
}
}
printf("%d\n",ans);
}
2.次小生成树
1.次小生成树
Qin Shi Huang’s National Road System
HDU - 4081
题意:有n个(n<=1000)城市,告诉坐标(int),边的权值就是两点的距离,并且每个城市都有人居住,现在要修路n-1条路,使得每个城市都连通。现能让一条边可以不用任何花费。求这条边的两端点的总人数/(包含这条边的最小生成树的总权值-这条边的权值(这条边花费为0时最小生成树的总权值))最大值。即(Wa+Wb)/(mst-w(a,b))最大。
观察上式,分母上是一条边的两个端点。分子上是包含这条边的最小生成树。我们考虑枚举每条边,如果它在MST上,就已经解决了。如果不在MST上,我们就找出加上它所形成的那个环,减去环上的最大边即可。
这里隐含了次小生成树的求法:枚举每条不在最小生成树上的边,加入树中,减掉环上的最大边。O(n^2)
sort(e + 1, e + k);
for(int i = 1; i < k; ++i){
int xx = find(e[i].u),yy = find(e[i].v);
if(xx != yy){
for(int a = head[xx]; a; a = ns[a].nxt)
for(int b = head[yy]; b; b = ns[b].nxt)
dis[ns[a].v][ns[b].v] = dis[ns[b].v][ns[a].v]=e[i].w;//因为是树,所以最短路唯一
ans += e[i].w;
fa[yy] = xx;
ns[endd[yy]].nxt = head[xx];//!!
head[xx] = head[yy];//!!
if(++cnt == n - 1) break;
}
}
3.最优比率生成树
01分数规划。
直接二分答案。重新给边权赋值,然后跑kruskal即可。
4.K度最小生成树
POJ 1639 Picnic Planning
求一个无向图的最小生成树,其中有一个点的度有限制(假设为 k)。
要求k度最小生成树,我们可以按照下面的步骤来做:
设有度限制的点为 RT。
1,把所有与RT相连的边删去,图会分成多个子图(假设为m个,如果m>k,那么问题无解),在内部分别求最小生成树;
然后用最小的代价将 m 个最小生成树和 RT连起来,那我们就得到了一棵关于RT的m度最小生成树。
2,在m度生成树中找一个和RT相连的点,把这条边加入MST,会生成一个环,删去环上的最大边。
完成一次 2 操作后得到的是m+1度最小生成树,以此类推,直到得到k度最小生成树。
5.最短路径生成树
51nod 1443
求一个图的一棵生成树,使得在生成树上从u到任一点的最短距离等于原图中从u到任一点的最短距离。这样的生成树叫最短路径树。你的任务是找出从u开始的最短路径树,并且这个树中所有边的权值之和要最小。
这类题一定要分清重要性。
题意是在满足最短路长度相同的情况下,生成树的权最小。所以应该先跑最短路,在最短路的基础上考虑第二个条件。
有两种做法:
1.由于生成树中的每条边会给树带来一个点,所以每个点的贡献就是最短路上的最小前驱。
2.先跑最短路,再记录所有可能是最短路上的边,然后跑最小生成树。
这是第一种做法。
不开longlong见祖宗。
void dijkstra(int s){
memset(dis,inf,sizeof(dis));
dis[s]=0;q.push(make_pair(0,s));
while(!q.empty()){
int u=q.top().second;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
val[v]=e[i].w;
q.push(make_pair(dis[v],v));
}
else if(dis[v]==dis[u]+e[i].w&&val[v]>e[i].w){
val[v]=e[i].w;
}
}
}
}
int a,b,c;
int main(){
// freopen("a.txt","r",stdin);
memset(head,-1,sizeof(head));
n=read();m=read();
for(int i=1;i<=m;++i){
a=read();b=read();c=read();
adde(a,b,c);adde(b,a,c);
}
s=read();
dijkstra(s);
for(int i=1;i<=n;++i){
ans+=val[i];
}
write(ans);putchar('\n');
return 0;
}
CH6202
求一个图中最短路径生成树的个数。
首先用最短路算法求出从u到任一点的最短路。然后将所有节点按dis从小到大排序。像Prim一样依次把每个节点P加入生成树。
统计有多少已经在树上的节点x有边连向P,且满足dis[p]=dis[x]+d[x][p].累乘即可。
二.最短路
1.最短路
dijkatra
vis[]记录每个点是否进入最短路集合。
typedef pair<int,int> pii;
priority_queue <pii,vector<pii>,greater<pii> >q;
void dijkstra(){
memset(dis,INF,sizeof(dis));
dis[s]=0;q.push(make_pair(dis[s],s));
while(!q.empty()){
int u=q.top().second;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
q.push(make_pair(dis[v],v));
}
}
}
}
spfa
vis[u]记录的是u点是否入队。
queue <int> q;
void spfa(int s){
//必须赋成INF
memset(dis,INF,sizeof(dis));
while(!q.empty()) q.pop();
q.push(s);in[s]=1;
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=0;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
if(!vis[v]){
vis[v]=1;q.push(v);
//判断负环
if((++in[v])>=n){
flg=1;return;
}
}
}
}
}
}
floyd
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
dis[i][j]=dis[i][k]+dis[k][j];
2.次短路
Roadblocks POJ - 3255
题意:求出1 -> n的次短路;(1 ≤ N ≤ 5000)
思路:
1.可以跑两遍最短路,从1 -> n 和 n -> 1 因为次短路肯定是替换最短路上的一条边后形成的,所以之后直接枚举每条边替换就行了。(在跑n -> 1的时候要建反向图)
2.在dijkstra的时候同时更新最短和次短。(推荐!)
区别于“从城市s1到t1不超过l1小时,并且从城市s2到t2不超过l2小时”那题,那题是必须枚举两条最短路重合的路径。
queue <int> q;
void spfa1(){
memset(dis1,inf,sizeof(dis1));
dis1[1] = 0;q.push(1);
while(!q.empty()) {
int u = q.front();q.pop();vis[u] = 0;
for(int i = head[u]; i ; i = e[i].nxt) {
int v = e[i].v;
if(dis1[v] > dis1[u] + e[i].w){
dis1[v] = dis1[u] + e[i].w;
if(!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
}
void spfan(){
memset(dis2,inf,sizeof(dis2));
dis2[n] = 0;q.push(n);
while(!q.empty()){
int u = q.front();q.pop();vis[u] = 0;
for(int i = head[u]; i ; i = e[i].nxt){
int v = e[i].v;
if(dis2[v] > dis2[u] + e[i].w){
dis2[v] = dis2[u] + e[i].w;
if(!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
}
int main(){
scanf("%d%d",&n,&r);
for(int i = 1; i <= r; ++i){
scanf("%d%d%d",&a,&b,&c);
adds(a,b,c); adds(b,a,c);
}
spfa1();spfan();ans = dis1[n];
for(int i = 1; i < k; ++i)
if(dis1[e[i].u] + dis2[e[i].v] + e[i].w > ans)
cnt = min(dis1[e[i].u] + dis2[e[i].v] + e[i].w,cnt);
printf("%d",cnt);
return 0;
}
3.最小环
1.无向图的最小环:FLOYD
path记录了最小环的路径。
vector <int> path;
void get_path(int x,int y){
if(pos[x][y]==0)return;
get_path(x,pos[x][y]);
path.push_back(pos[x][y]);
get_path(pos[x][y],y);
}
int main(){
n=read();m=read();
memset(a,INF,sizeof(a));
for(int i=1;i<=n;++i) a[i][i]=0;//!!
for(int i=1,x,y,z;i<=m;++i){
x=read();y=read();z=read();
a[x][y]=a[y][x]=min(a[x][y],z);
}
memcpy(d,a,sizeof(a));
for(int k=1;k<=n;++k){
for(int i=1;i<k;++i)
for(int j=i+1;j<k;++j)
if((ll)d[i][j]+a[j][k]+a[k][i]<ans){
ans=d[i][j]+a[j][k]+a[k][i];
path.clear();
path.push_back(i);
get_path(i,j);
path.push_back(j);
path.push_back(k);
}
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
if(d[i][j]>d[i][k]+d[k][j]){
d[i][j]=d[i][k]+d[k][j];
pos[i][j]=k;
}
}
if(ans==INF){
puts("No solution.");
return 0;
}
int sz=path.size();
for(int i=0;i<sz;++i) printf("%d ",path[i]);
return 0;
}
2.有向图的最小环
0 or 1 HDU - 4370
题意:给你一个矩阵C,让你构造一个01矩阵X使 ∑ i = 1 , j = 1 i < = n , j < = n C i j ∗ X i j ∑_{i=1,j=1}^{i<=n,j<=n}Cij*Xij ∑i=1,j=1i<=n,j<=nCij∗Xij最小,输出这个最小值。
构造出来的X矩阵需要满足一下关系:
1.$X_{12}+X_{13}+…+X_{1n}=1 $
2.$X_{1n}+X_{2n}+…X_{n-1n}=1 $
3.对于任意的 i ( 1 < i < n ) i (1 < i < n) i(1<i<n), 满足 $\sum{X_{ki}(1<=k<=n)} =\sum{X_{ij}(1<=j<=n)} $.
思维题:
将矩阵C看做描述N个点的邻接矩阵
再来看三个条件:
条件一:表示1号点出度为1
条件二:表示n号点入度为1
条件三:表示k( 1 < k < n )号点出度等于入度
由此题目要求的 ∑ i = 1 , j = 1 i < = n , j < = n C i j ∗ X i j ∑_{i=1,j=1}^{i<=n,j<=n}Cij*Xij ∑i=1,j=1i<=n,j<=nCij∗Xij就变为从一个完全图中选一些边,形成以下两种路径之一:
1.1号点到n号点的花费
2.形成一个含1号点的环(非自环)和一个含n号点的环(非自环)的花费之和(因为1到1号点和n号点到n号点是不受限制的)
一个包含u的环的最小花费计算方式如下:(用dijkstra或spfa做基础均可)
将u直达的v的dis赋成w[u,v]然后入队,其余所有点的dis赋成inf(包括u本身,特判自环),通过最短路算法的松弛,dis[u]即为包含u的最小环的花费。
void init(int s){
for(int i = 1; i <= n; ++i){
if(i == s) dis[i] = inf;
else if(g[s][i] != inf) {
dis[i] = g[s][i];
q.push(i);
}
else dis[i] = inf;
}
}
void spfa(int s){
init(s);
while(!q.empty()){
int u = q.front();q.pop();vis[u] = 0;
for(int v = 1; v <= n; ++v){
if(v == u) continue;
if(dis[v] > dis[u] + g[u][v]){
dis[v] = dis[u] + g[u][v];
if(!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
}
int main(){
while(~scanf("%d",&n)){
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= n; ++j)
scanf("%d",&g[i][j]);
spfa(1);//在求包含1号点的最小环时顺带求出从1到n的路径。
ans = dis[n];
tmp = dis[1];
spfa(n);
ans = min(ans,dis[n] + tmp);
printf("%d\n",ans);
}
}
4.传递闭包
与topsort的区别在于传递闭包是计算当前点和所有点的关系。而topsort是top序的关系
Cow Contest POJ - 3660 题意:有n头牛比赛,m个比赛结果,最后问你一共有多少头牛的排名被确定了,其中如果a战胜b,b战胜c,则也可以说a战胜c。求能确定排名的牛的数目。(1 ≤ N ≤ 100)
数据保证结果不存在矛盾。
第一想法肯定是拓扑排序的,但是这题求的不是a输给哪几头牛,而是a输给了某些牛且战胜了其余的所有牛。
所以我们考虑一头牛被x头牛打败,打败y头牛(即入度x出度y)且x+y=n-1,则这头牛的排名就被确定了。
所以我们只要将任何两头牛的胜负关系确定了,在遍历所有牛判断一下是否满足x+y=n-1,将满足这个条件的牛数目加起来就是所求解。
任意两头牛的胜负关系是简单的传递闭包问题,极小的数据范围可以用folyd解决。
int main(){
scanf("%d%d",&n,&m);
for(int i = 1; i <= m; ++i) {
scanf("%d%d",&a,&b);
dis[a][b] = 1;
}
for(int k = 1; k <= n; ++k)
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= n; ++j)
dis[i][j] |= (dis[i][k] && dis[k][j]);
for(int i = 1; i <= n; ++i) {
cnt = 0;
for(int j = 1; j <= n; ++j)
if(dis[i][j] || dis[j][i]) ++cnt;
if(cnt == n - 1) ++ans;
}
printf("%d",ans);
return 0;
}
5.同余最短路
51NOD 1326 遥远的旅途
一个国家有N个城市,这些城市被标为0,1,2,…N-1。这些城市间连有M条道路,每条道路连接两个不同的城市,且道路都是双向的。一个小鹿喜欢在城市间沿着道路自由的穿梭,初始时小鹿在城市0处,它最终的目的地是城市N-1处。小鹿每在一个城市,它会选择一条道路,并沿着这条路一直走到另一个城市,然后再重复上述过程。每条道路会花费小鹿不同的时间走完,在城市中小鹿不花时间逗留。路程中,小鹿可以经过一条路多次也可以经过一个城市多次。给定城市间道路的信息,问小鹿是否有一种走法,从城市0出发到达城市N-1时,恰好一共花费T个单位的时间。如果存在输出“Possible”,否则输出“Impossible”。
注意,小鹿在整个过程中可以多次经过城市N-1,只要最终小鹿停在城市N-1即可。
把每一条某一个端点为0的边当成环,定义 d p [ i ] [ j ] dp[i][j] dp[i][j]为到达第i号点,路程%2w为j的路径的最小值。
用spfa跑最短路更新。如果 d p [ i ] [ L % 2 w ] < = L dp[i][L\%2w]<=L dp[i][L%2w]<=L,那么一定能通过补环成为L.
但是这是单向的。为什么所有这样的边都不满足就可以说无法到达呢:spfa的更新方式把所有在0到n-1路径上的环就跑了,只差与1相连的环。
如:用 d p [ u ] [ i ] − > d p [ v ] [ ( i + w ) % K ] − > d p [ u ] [ ( i + 2 ∗ w ) % k ] dp[u][i]\ \ ->\ \ dp[v][(i+w)\%K]\ \ ->\ \ dp[u][(i+2*w)\%k] dp[u][i] −> dp[v][(i+w)%K] −> dp[u][(i+2∗w)%k]
typedef pair<ll,ll> pll;
queue <pll> q;
void spfa(ll k){
memset(dis,INF,sizeof(dis));
q.push(make_pair(1LL,0LL));dis[1][0]=0;
while(!q.empty()){
ll u=q.front().first;ll d=q.front().second;q.pop();
vis[u][d]=0;
for(ll i=head[u];i!=-1;i=e[i].nxt){
ll v=e[i].v,t=(d+e[i].w)%k;
if(dis[v][t]>dis[u][d]+e[i].w){
dis[v][t]=dis[u][d]+e[i].w;
if(!vis[v][t]){
q.push(make_pair((ll)v,(ll)t));
vis[v][t]=1;
}
}
}
}
}
int main(){
T=read();
while(T--){
memset(head,-1,sizeof(head));kk=0;
flg=0;
n=read();m=read();L=read();
for(ll i=1,a,b,c;i<=m;++i){
a=read()+1;b=read()+1;c=read();
adde(a,b,c);adde(b,a,c);
}
for(ll i=0;i<kk;++i)
if(e[i].v==1){
spfa(2*e[i].w);
if(dis[n][L%(2*e[i].w)]<=L){
flg=1;
break;
}
}
if(flg) puts("Possible");
else puts("Impossible");
}
return 0;
}
拓扑排序
这类题一般topsort,并查集,folyd传递闭包
确定比赛名次 HDU - 1285
大意:有N个比赛队(1<=N<=500),编号依次为1,2,3,,,N进行比赛。现在知道每场比赛的结果,即P1赢P2,用P1,P2表示,排名时P1在P2之前。如果无法区分就按字典序从小到大输出。现在请你编程序确定队伍的排名。
topsort+priority_queue
tarjan
无向图的割点与桥
割点的判断:
u是割点:当且仅当u存在一个儿子v使得low[v]>=dfn[u].
因为是大于等于,所有不用考虑重边和父节点的问题。
cnt[]表示如果u是割点,割掉它后形成的联通块数目。
void tarjan(int u,int fa){
dfn[u] = low[u] = ++idx;
int son = 0;
for(int i = head[u]; i != -1; i = e[i].nxt){
int v=e[i].v;
if(!dfn[v]){
tarjan(v,u);
low[u] = min(low[u],low[v]);
++son;
if(u != rt && low[v] >= dfn[u]) cnt[u]++;
}
else low[u] = min(low[u],dfn[v]);
}
if(u == rt && son >= 2) cnt[u] = son;
else cnt[u]++;
if(cnt[u] >= 2) cut++;
}
桥的判断:
u到v的边是桥:当且仅当这条边连接的儿子low[v]>dfn[u].
所以需要考虑父节点和重边的问题。优秀的解决方案是记录进入边的编号。
void Tarjan(int u,int in_edge){
dfn[u]=low[u]=++idx;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v=e[i].v;
if(!dfn[v]){
Tarjan(v,i);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x])
bri[i]=bri[i^1]=1;
}
else if(i!=(in_edge^1))
low[u]=min(low[u],dfn[v]);
}
}
无向图的双联通分量(点双联通,边双联通)
点双联通:不存在割点。
边双联通:不存在桥。
点双联通:当且仅当满足下列俩条件之一:
1.图的顶点数不超过2
2.图中任意两点都同时包含在至少一个简单环中。
边双联通:当且仅当任意一条边都包含在至少一个简单环中。
点双联通:缩点后一个割点可能存在于多个联通块中。
POJ 3177 Redundant Paths
题意:有n个牧场,Bessie要从一个牧场到另一个牧场,要求至少要有2条独立的路可以走。现已有m条路,求至少要新建多少条路,使得任何两个牧场之间至少有两条独立的路。两条独立的路是指:没有公共边的路,但可以经过同一个中间顶点。
在点双联通分量缩点之后,每次去连一条链的两端(就是度为1的,设有sum个)。就是 s u m + 1 2 \frac{sum+1}{2} 2sum+1
注意点双联通就需要判断父亲。
void Tarjan(int u,int fa){
sta[++tp]=u;ins[u]=1;
dfn[u]=low[u]=++idx;
for(int i=head[u];i!=-1;i=e[i].nxt){
if(i==(fa^1)) continue;//!!
int v=e[i].v;
if(!dfn[v]){
Tarjan(v,i);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
++cnt;
while(1){
int v=sta[tp--];ins[v]=0;
col[v]=cnt;
if(u==v) break;
}
}
}
int main(){
// freopen("a.txt","r",stdin);
memset(head,-1,sizeof(head));
n=read();m=read();
for(int i=1,a,b;i<=m;++i){
a=read();b=read();
adde(a,b);adde(b,a);
}
Tarjan(1,-1);
for(int i=1;i<=n;++i)
for(int j=head[i];j!=-1;j=e[j].nxt){
int v=e[j].v;
if(col[i]!=col[v]) d[col[i]]++;
}
for(int i=1;i<=cnt;++i)
if(d[i]==1) sum++;
printf("%d\n",(sum+1)/2);
return 0;
}
错误做法:利用双连通分量内的点low[]值都是相同的,在dfs时,用low[u]=min(low[u],low[v]),(即用low[v]替代dfn[v]),就不缩点,在求度数时,枚举每条边判断low[u]是否等于low[v],若low[u]!=low[v],则不是同一个边双连通分量,度数+1。
从一开始就是错的。关键在于同一个边双连通分量内的点low[]值不一定相同.如果这个边双联通分量中有多个环,就不成立。详见参考。
数据
4 5
1 2
1 3
2 3
2 4
3 4
边双联通:去掉所有桥之后再跑图即可。桥不存在于任何联通块。
有向图的强连通分量
就是点双联通分量的有向图版本。因为是有向图,就不用担心重边和父节点的问题。
void tarjan(int u){
s.push(u);ins[u] = 1;
dfn[u] = low[u] = ++idx;
for(int i = head[u]; i != -1; i = e[i].nxt){
if(!dfn[e[i].v]){
tarjan(e[i].v);
low[u] = min(low[u],low[e[i].v]);
}
else if(ins[e[i].v]) low[u] = min(low[u],dfn[e[i].v]);
}
if(dfn[u] == low[u]){
++scc;
while(s.top() != u) {
ins[s.top()] = 0;
s.pop();
}
s.pop();//u要出栈。
}
}
2-SAT问题
什么是2-sat问题
有n个布尔型变量xi,另外m个需要满足的条件。每个条件都是“xi为真/假或者xj为真/假”。这句话中的“或者”意味着两个条件中至少有一个正确。2-sat问题的目标是给每个变量赋值,使得所有的条件得到满足。
算法的大致过程是这样的:
构造一张有向图G,其中每个变量拆成两个结点2i和2i+1,分别表示xi为假和xi为真。最后要为每个变量选其中一个结点标记。
对于每个“xi为假或者xj为假"这样的条件,我们连两条对称的有向边。我们上面说过,或者意味着两个中间至少有一个正确。所以如果xi为真的话,那么xj一定为假,所以我们从2i+1向2j连一条有向边。同样的道理,我们也从2j+1向2i连一条有向边。
接下来我们逐一考虑每个没有赋值的变量,设为xi。我们先假定它为假,然后标记结点2i,并且沿着有向边标记所有能标记的结点。如果标记的过程中发现某个变量对应的两个结点都被标记,则xi假这个假定不成立,需要改为xi为真,然后重新标记。
这种方法太过暴力,只适用于求字典序最小的时候。
所以我们可以用Tarjan优化。求出图中的强连通分量。如果真假两个节点在同一个联通块中就无解。否则有解。
注意事项:
1.有向图 or 无向图
2.图是否一定联通
3.kk有无清零,head有没有赋成-1
4.队列有无pop()
参考:
https://www.cnblogs.com/chenchengxun/p/4718736.html
http://www.cnblogs.com/LQLlulu/p/9310860.html
https://blog.csdn.net/M_GSir/article/details/52892381