图论 | 模板题整理
// 好些天没有更新博客了,昨天的日记也提到了上个部分的学习难度较大,很多代码并不是自己手写,就没来得及消化。
// 今天有空就把代码整理一遍,参考的博客地址也都写在了代码前的注释中。
Day 6~7 图论基础
这天开始引入的例题食物链是一道典型的利用并查集解决的问题:
题目描述:现在给出三个种族A, B, C,已知它们之间的关系是A吃B B吃C,C吃A,构成了一种奇妙的环状关系。现在给出n个动物构成 的m组信息。信息分为两种,第一种的形式为1 u v,表示u与v为同 类;第二种的形式为2 u v,表示u吃v。试统计有多少组矛盾信息。
这也是练习题B题 B - 食物链(POJ - 1182 )
#include <iostream> #include <cstdio> using namespace std; // https://blog.csdn.net/chaiwenjun000/article/details/50202979 // https://blog.csdn.net/zcmartin2014214283/article/details/50898722 const int maxn = 50010; int fa[maxn*3]; int find(int x) { return x==fa[x]?x:fa[x]=find(fa[x]); } bool same(int x, int y) { return find(x)==find(y); } void Union(int x, int y) { int xx = find(x), yy = find(y); if(xx==yy) return; fa[xx] = yy; } int main() { int n, k; scanf("%d %d", &n, &k); for(int i=0;i<=3*n;i++) fa[i] = i; int d, x, y, ans = 0; for(int i=0;i<k;i++) { scanf("%d %d %d", &d, &x, &y); if(x>n || y>n) { ans++; continue; } if(d==1) { if(same(x, y+n)||same(x, y+2*n)) ans++; else { Union(x, y); Union(x+n, y+n); Union(x+2*n, y+2*n); } } else { if(same(x, y)||same(x,y+2*n)) ans++; else { Union(x, y+n); Union(x+n,y+2*n); Union(x+2*n, y); } } } printf("%d\n", ans); return 0; }
做练习题时第一题A - How Many Answers Are Wrong(HDU - 3038 )属于带权并查集,需要在上面find函数上增加一个sum[N]数组来维护区间fa[i]到i之间的和。注意读入的左端点值要减一。
const int N = 200010;
int fa[N], sum[N];int find(int x)
{
if(x==fa[x]) return x;int f = find(fa[x]);
sum[x] += sum[fa[x]];
return fa[x] = f;
}
接下来主要讲解了Tarjan算法求强连通分量,这一块我不是很懂,练习题也大都直接略过了。贴一个已解决的E - SPF(POJ - 1523 )求割点的代码:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; // https://blog.csdn.net/xc19952007/article/details/48349055 #define CLR(a) memset(a, 0, sizeof(a)) const int MAX = 1010; int head[MAX], cnt, res[MAX]; struct Node { int to, nex; }e[100005]; void add(int u, int v) { ++cnt; e[cnt].to = v; e[cnt].nex = head[u]; head[u] = cnt; } int number[MAX], low[MAX]; void dfs(int u, int d) { int cc = 0; number[u] = low[u] = d; for(int i = head[u];~i;i=e[i].nex) { int v = e[i].to; if(!number[v]) { dfs(v, d+1); cc++; low[u] = min(low[u], low[v]); res[u] += u==1?cc>1:low[v]>=number[u]; } else low[u] = min(low[u], number[v]); } } void fun() { bool flag = 0;; for(int i=1;i<1010;i++) if(res[i]) { printf(" SPF node %d leaves %d subnets\n", i, res[i]+1); flag = 1; } if(!flag) printf(" No SPF nodes\n"); } void Init() { // memset(e, 0, sizeof(e)); memset(head, -1, sizeof(head)); // CLR(e); CLR(number); CLR(low); CLR(res); cnt = 0; } int main() { int u, v, t = 0; while(scanf("%d", &u)==1 && u) { scanf("%d", &v); Init(); add(u, v); add(v, u); while(scanf("%d", &u)==1 && u) { scanf("%d", &v); add(u, v); add(v, u); } dfs(1, 1); printf("Network #%d\n", ++t); fun(); printf("\n"); } return 0; }
后来补的H - Highways( POJ - 1751 )这是一个最小生成树的模板题,也就是Day 8的主要内容,采用的Prim算法求解最小生成树。
#include<iostream> #include<algorithm> #include<stdio.h> #include<string.h> #include<math.h> using namespace std; //普利姆,注意建图技巧 const int maxn=751; const int INF=0x3f3f3f3f; int map[maxn][maxn]; int dis[maxn]; int vis[maxn]; int Edge[maxn];//i到Edge[i]是一条生成树内的边 struct node { int x; int y; } Point[maxn]; //第i个点的坐标 int N;//点的数量 int M;//更新边的数量 void init() { scanf("%d",&N); for(int i=1; i<=N; i++)//建图 { scanf("%d%d",&Point[i].x,&Point[i].y); for(int j=1; j<i; j++)//为什么这里不取sqrt,因为完全没必要 map[i][j]=map[j][i]=(Point[i].x-Point[j].x)*(Point[i].x-Point[j].x)+(Point[i].y-Point[j].y)*(Point[i].y-Point[j].y); map[i][i]=INF;//自己不可能到自己 } scanf("%d",&M); int x,y; while(M--)//更新图 { scanf("%d%d",&x,&y); map[x][y]=map[y][x]=0; } memset(vis,0,sizeof(vis)); vis[1]=1; for(int i=1; i<=N; i++) { dis[i]=map[i][1]; Edge[i]=1;//初始化为存储i到1的边 } } void Prim() { for(int i=1; i<N; i++) { int minn=INF; int point_minn; for(int j=1; j<=N; j++) if(vis[j]==0&&minn>dis[j]) { minn=dis[j]; point_minn=j; } vis[point_minn]=1; for(int k=1; k<=N; k++) if(vis[k]==0&&dis[k]>map[point_minn][k]) { Edge[k]=point_minn;//这里是输出方式的技巧 dis[k]=map[point_minn][k]; } if(map[Edge[point_minn]][point_minn]) printf("%d %d\n",Edge[point_minn],point_minn); } } int main() { init(); Prim(); return 0; }
Day 8 生成树相关扩展
相关问题有以下内容:
- 次小生成树
- 最小度限制生成树
- 最优比率生成树
- 最小树形图
似乎除了最优比率生成树以外我都没掌握,下面是由01分数规划过渡到最优比率生成树的问题题解。
01分数规划就是给定两个数组,a[i]表示选取i的收益,b[i]表示选取i的代价,求一个选取方案 使 取得最优解。解决思路就是二分搜索。
B - Dropping tests( POJ - 2976 )
#include <cstdio> #include <iostream> #include <algorithm> using namespace std; int a[1010], b[1010]; double c[1010]; bool cmp(double a, double b) { return a>b; } int main() { int n, k; while(scanf("%d %d", &n, &k)!=EOF && n) { for(int i=0;i<n;i++) scanf("%d", &a[i]); for(int i=0;i<n;i++) scanf("%d", &b[i]); double l = 0, r = 1, mid = 0.5; while(r-l>=0.0001) // WA: 0.001 { mid = (l+r)/2; for(int i=0;i<n;i++) c[i] = a[i] - mid*b[i]; sort(c, c+n, cmp); bool flag = 1; double sum = 0; for(int i=0;i<n-k;i++) { sum += c[i]; if(sum<0) {flag = 0; break;} } // cout<<flag<<' '<<mid<<endl; if(flag) l = mid; else r = mid; } printf("%1.f\n", 100*l); // OR: %d,(int)(100*l+0.5) } return 0; }
最优比率生成树:
#include <cstdio> #include <iostream> #include <algorithm> #include <cmath> #include <cstring> using namespace std; // https://blog.csdn.net/chenzhenyu123456/article/details/48160209 int n; struct node{ int x, y, h; }P[1010]; double map[1010][1010], len[1010][1010], cost[1010][1010]; double low[1010], Max; bool vis[1010]; double Dis(double x, double y) { return sqrt(x*x+y*y); } double prim() { double res = 0; for(int i=1;i<=n;i++) { vis[i] = 0; low[i] = map[i][1]; } vis[1] = 1; for(int i=1;i<n;i++) { double minn = 20000000; int nxt, flag = 1; for(int j=1;j<=n;j++) { if(!vis[j] && minn>low[j]) { minn = low[j]; nxt = j; flag = 0; } } if(flag) break; res += minn; vis[nxt] = 1; for(int j=1;j<=n;j++) { if(!vis[j] && low[j]>map[nxt][j]) low[j] = map[nxt][j]; } } // printf("%lf\t", res); return res; } void init() { Max = 0; for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) { cost[i][j] = cost[j][i] = abs(P[i].h-P[j].h); Max = max(Max, cost[i][j]/len[i][j]); } } bool judge(double k) { for(int i=1;i<=n;i++) { for(int j=i+1;j<=n;j++) map[i][j] = map[j][i] = cost[i][j] - k*len[i][j]; } if(prim()>=0) return true; return false; } int main() { while(scanf("%d", &n)!=EOF && n) { for(int i=1;i<=n;i++) { scanf("%d %d %d", &P[i].x, &P[i].y, &P[i].h); for(int j=1;j<i;j++) len[i][j] = len[j][i] = Dis(P[i].x-P[j].x, P[i].y-P[j].y); } init(); // for(int i=1;i<=n;i++,cout<<endl) // for(int j=i+1;j<=n;j++) // printf("(%lf %lf)", len[i][j], cost[i][j]); double l = 0, r = Max, mid; while(r-l>=1e-6) { mid = (l+r)/2; // printf("%lf\n", mid); if(judge(mid)) l = mid; else r = mid; } printf("%.3lf\n", l); } return 0; }
然后解(copy)决(code)了好几个最小点覆盖问题,这里直接先上结论:对于二分图,1.最小点覆盖=最大匹配 2.最大独立集=点的个数-最小点覆盖。
因此求最小点覆盖数也就是求最大匹配数,最大独立集也都能用相同的算法求解。下面的代码采用了匈牙利算法。
G - Machine Schedule( POJ - 1325 )
/* 顶点编号从0开始的 邻接矩阵(匈牙利算法) 二分图匹配(匈牙利算法的DFS实现)(邻接矩阵形式) 初始化:g[][]两边顶点的划分情况 建立g[i][j]表示i->j的有向边就可以了,是左边向右边的匹配 g没有边相连则初始化为0 uN是匹配左边的顶点数,vN是匹配右边的顶点数 左边是X集,右边是Y集 调用:res=hungary();输出最大匹配数 优点:适用于稠密图,DFS找增广路,实现简洁易于理解 时间复杂度:O(VE) */ #include <iostream> #include <cstdio> #include <cstring> using namespace std; const int MAXN=110; int uN,vN;//u,v 的数目,使用前面必须赋值 int g[MAXN][MAXN];//邻接矩阵,记得初始化 int linker[MAXN];//linker[v]=u,表示v(右边Y集合中的点)连接到u(左边X集合中的点) bool used[MAXN]; bool dfs(int u) {//判断以X集合中的节点u为起点的增广路径是否存在 for(int v=0;v<vN;v++)//枚举右边Y集合中的点 if(g[u][v]&&!used[v]){//搜索Y集合中所有与u相连的未访问点v used[v]=true;//访问节点v if(linker[v]==-1||dfs(linker[v])){//是否存在增广路径 //若v是未盖点(linker[v]==-1表示没有与v相连的点,即v是未盖点),找到增广路径 //或者存在从与v相连的匹配点linker[v]出发的增广路径 linker[v]=u;//设定(u,v)为匹配边,v连接到u return true;//返回找到增广路径 } } return false; } int hungary() {//返回最大匹配数(即最多的匹配边的条数) int res=0;//最大匹配数 memset(linker,-1,sizeof(linker));//匹配边集初始化为空 for(int u=0;u<uN;u++){//找X集合中的点的增广路 memset(used,false,sizeof(used));//设Y集合中的所有节点的未访问标志 if(dfs(u))res++;//找到增广路,匹配数(即匹配边的条数)+1 } return res; } int main() { int i, ans; int K; int p, R, C; while(~scanf("%d",&uN) && uN) { scanf("%d %d", &vN, &K); memset(g,0,sizeof(g)); while(K--) { scanf("%d%d%d",&p,&R,&C); if(R>0&&C>0) g[R][C]=1; } ans = hungary(); printf("%d\n",ans); } return 0; }
当我一直用上个模板到Day 11的dp进阶专题时,有道题P - Strategic game( POJ - 1463 )出现了TLE,上网一查发现用邻接矩阵的匈牙利算法效率会有些低,所以又抄了一份邻接表的写法:
/* HDU 1054 用STL中的vector建立邻接表实现匈牙利算法 效率比较高 */ #include<stdio.h> #include<iostream> #include<algorithm> #include<string.h> #include<vector> using namespace std; // https://www.cnblogs.com/kuangbin/archive/2012/08/19/2646713.html //************************************************ const int MAXN=1505; int linker[MAXN]; bool used[MAXN]; vector<int>map[MAXN]; int uN; bool dfs(int u) { for(int i=0;i<map[u].size();i++) { if(!used[map[u][i]]) { used[map[u][i]]=true; if(linker[map[u][i]]==-1||dfs(linker[map[u][i]])) { linker[map[u][i]]=u; return true; } } } return false; } int hungary() { int u; int res=0; memset(linker,-1,sizeof(linker)); for(u=0;u<uN;u++) { memset(used,false,sizeof(used)); if(dfs(u)) res++; } return res; } //***************************************************** int main() { int u,k,v; int n; while(scanf("%d",&n)!=EOF) { for(int i=0;i<MAXN;i++) map[i].clear(); for(int i=0;i<n;i++) { scanf("%d:(%d)",&u,&k); while(k--) { scanf("%d",&v); map[u].push_back(v); map[v].push_back(u); } } uN=n; printf("%d\n",hungary()/2); } return 0; }
//Day 9 网络流
// 这天一直在补前面的生成树的题,练习题迟迟没做以致最后爆零T.T
// 这块真的要等到学好了图论基础再来,目前只知道两个基本概念——最大流和最小割,什么增广路算法和Dinic算法有待后续理解和掌握0.0
未完待续......