三、拓扑排序
1. hdu 1285 确定比赛次序
题意:给出n支队伍参与比赛的胜负关系,根据胜负关系对所有队伍进行排名,要求两支队伍之间若有胜负关系,则赢的排在前,如果有多组符合题意的解,则输出字典序小的解。
思路:直接进行拓扑排序输出结果即可。
注意:题中可能有重边,要对重边进行处理,重边会导致顶点的入度出现错误。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
int mp[505][505], indeg[505], res[505];
void toposort(int n)
{
int rt = 0;
for(int i = 1; i <= n; ++ i)
{
for(int j = 1; j <= n; ++ j)
{
if(indeg[j] == 0)
{
res[++ rt] = j;
indeg[j] -= 1;
for(int k = 1; k <= n; ++ k)
{
if(mp[j][k] == 1)
indeg[k] -= 1;
}
break;
}
}
}
}
int main(void)
{
int n, m, u, v;
while(scanf("%d%d", &n, &m) != EOF)
{
memset(mp, 0, sizeof(mp));
memset(indeg, 0, sizeof(indeg));
memset(res, 0, sizeof(res));
for(int i = 1; i <= m; ++ i)
{
scanf("%d%d", &u, &v);
if(mp[u][v] == 0)
indeg[v] += 1;
mp[u][v] = 1;
}
toposort(n);
for(int i = 1; i < n; ++ i)
printf("%d ", res[i]);
printf("%d\n", res[n]);
}
return 0;
}
2. hdu 3342 Legal or not
题意:两人间可能存在master-prentice关系,master可有多个prentice,prentice也可有多个master,关系具有传递性,但不能存在一个人既是另一个人的master又是同一个人的prentice,判断当前关系是否合法。
思路:该题为判断图中是否有环,可用拓扑排序,在拓扑排序进行到某一步,如果不存在入度为0的点,则图中有环。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
int mp[105][105], indeg[105];
int toposort(int n)
{
for(int i = 1; i <= n; ++ i)
{
int m = 0, x = -1;
for(int j = 0; j <= n - 1; ++ j)
{
if(indeg[j] == 0)
{
m ++;
x = j;
}
}
if(m == 0)
return 0;
indeg[x] -= 1;
for(int j = 0; j <= n - 1; ++ j)
{
if(mp[x][j] == 1)
indeg[j] -= 1;
}
}
return 1;
}
int main(void)
{
int n, m, u, v;
while(scanf("%d%d", &n, &m) != EOF && n != 0)
{
memset(mp, 0, sizeof(mp));
memset(indeg, 0, sizeof(indeg));
for(int i = 1; i <= m; ++ i)
{
scanf("%d%d", &u, &v);
if(mp[u][v] == 0)
indeg[v] += 1;
mp[u][v] = 1;
}
int res = toposort(n);
if(res == 1)
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
3. hdu 2647 Reward
题意:分配奖金,每个人最少分配888元,员工会提出某些要求,要求员工a的工资高于员工b的工资。求满足所有员工要求的情况下最少需要分配多少钱的奖金。
思路:考虑到员工人数n为10000,因此不再用邻接矩阵的方式,使用邻接表。由于员工要求为a工资高于b,故建图时建一条从b指向a的边,这样建图完成后入度为0的点就是分配最少奖金的员工。使用队列记录所有当前入度为0的结点,每次出队一个结点后,将其连接的下一个顶点的入度减1,如果下一个顶点入读减为0,那么下一个顶点的分配奖金数为出队结点加1,并将这个结点入队,直至队列中无入度为0的结点。每次结点出队时累加结点分配的奖金数即可。记录出队的结点总数,如果队列为空后,出队的结点数量小于结点总数,说明图中有环,因此解不存在。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
vector <int> g[10005];
int indeg[10005], rew[10005];
int solve(int n)
{
int ans = 0, cnt = 0;
queue <int> q;
memset(rew, 0, sizeof(rew));
for(int i = 1; i <= n; ++ i)
{
if(indeg[i] == 0)
{
rew[i] = 888;
q.push(i);
}
}
while(q.empty() == 0)
{
int p = q.front();
q.pop();
cnt ++;
ans += rew[p];
for(int i = 0; i < g[p].size(); ++ i)
{
int t = g[p][i];
indeg[t] -= 1;
if(indeg[t] == 0)
{
rew[t] = rew[p] + 1;
q.push(t);
}
}
}
if(cnt < n)
return -1;
return ans;
}
int main(void)
{
int n, m, u, v;
while(scanf("%d%d", &n, &m) != EOF)
{
memset(indeg, 0, sizeof(indeg));
memset(g, 0, sizeof(g));
for(int i = 1; i <= m; ++ i)
{
scanf("%d%d", &u, &v);
int flag = 0;
for(int j = 0; j < g[v].size(); ++ j)
{
if(g[v][j] == u)
flag = 1;
}
if(flag == 0)
{
g[v].push_back(u);
indeg[u] += 1;
}
}
printf("%d\n", solve(n));
}
return 0;
}
4. hdu 1811 Rank of Tetris
题意:给出游戏中已知的两个人的rating关系,包括>,<,=。求是否能够确定n个人的排名,其中如果两人之间为=关系,那么编号大的排在前面,关系可以传递。
思路:首先使用并查集将所有具有=关系的各个点合并,接下来在拓扑排序过程中所有的点都是用其所在集合的根节点来进行处理,相当于缩点。接下来,进行拓扑排序过程。在排序过程中,如果某一时刻有多个入度为0的点,则无法确定uncertain,如果队列为空时,还有未入队的根节点,那么有环路,则存在冲突conflict。
注意:该题求解过程中注意事项较多。(1)拓扑排序过程中所有的点都是用所在集合的根节点,因此最后判断是否有环路时使用的总数不再是结点总数n,而是集合数量,因为这个问题wrong好几次。(2)拓扑排序过程中,首先应将起点即入度为0的点加入队列,这时要注意入队的点还必须是集合根节点,也就是说初始的入队条件是indeg[i] == 0 && i = find_rt(i),不是所有入度为0的点都加入队列。(3)注意如果既不确定又冲突输出conflict,因此判断出不确定后不能立即输出答案,要继续判断其是否还有冲突,如果找到冲突则可以直接输出冲突。(4)注意m = 0的情况,n > 1时为uncertain。(5)注意重边的处理。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
vector <int> g[10005];
int indeg[10005], pre[10005], rnk[10005], u[20005], v[20005];
char co[20005];
void init(int n)
{
for(int i = 0; i < n; ++ i)
{
pre[i] = i;
rnk[i] = 0;
}
}
int find_rt(int x)
{
if(x == pre[x])
return x;
return pre[x] = find_rt(pre[x]);
}
int union_rnk(int x, int y)
{
int px = find_rt(x);
int py = find_rt(y);
if(px == py)
return 0;
if(rnk[px] < rnk[py])
pre[px] = py;
else
{
pre[py] = px;
if(rnk[px] == rnk[py])
rnk[px] ++;
}
return 1;
}
int toposort(int n, int num)
{
queue <int> q;
int cnt = 0, cnt_z = 0, flag = 2;//flag: 2-ok,1-uncertain,0-conflict
for(int i = 0; i < n; ++ i)
{
if(indeg[i] == 0 && find_rt(i) == i)
{
cnt_z += 1;
q.push(i);
}
}
if(cnt_z > 1)
flag = 1;
while(q.empty() == 0)
{
int t = q.front();
q.pop();
cnt ++;
cnt_z = 0;
for(int i = 0; i < g[t].size(); ++ i)
{
int to = g[t][i];
indeg[to] -= 1;
if(indeg[to] == 0)
{
q.push(to);
cnt_z += 1;
}
}
if(cnt_z > 1)
flag = 1;
}
if(cnt < num)
return 0;
return flag;
}
int main(void)
{
int n, m;
int fflag;
while(scanf("%d%d", &n, &m) != EOF)
{
int num = n;
if(n > 1 && m == 0)
{
printf("UNCERTAIN\n");
continue;
}
memset(indeg, 0, sizeof(indeg));
memset(g, 0, sizeof(g));
for(int i = 1; i <= m; ++ i)
scanf("%d %c %d", &u[i], &co[i], &v[i]);
init(n);
for(int i = 1; i <= m; ++ i)
{
if(co[i] == '=')
num = num - union_rnk(u[i], v[i]);
}
fflag = 1;
for(int i = 1; i <= m; ++ i)
{
if(co[i] == '>')
{
int pu = find_rt(u[i]);
int pv = find_rt(v[i]);
if(pu == pv)
{
fflag = 0;
break;
}
int fg = 0;
for(int j = 0; j < g[pu].size(); ++ j)
{
if(g[pu][j] == pv)
{
fg = 1;
break;
}
}
if(fg == 0)
{
g[pu].push_back(pv);
indeg[pv] += 1;
}
}
else if(co[i] == '<')
{
int pu = find_rt(u[i]);
int pv = find_rt(v[i]);
int fg = 0;
if(pu == pv)
{
fflag = 0;
break;
}
for(int j = 0; j < g[pv].size(); ++ j)
{
if(g[pv][j] == pu)
{
fg = 1;
break;
}
}
if(fg == 0)
{
g[pv].push_back(pu);
indeg[pu] += 1;
}
}
}
if(fflag == 0)
printf("CONFLICT\n");
else
{
int res = toposort(n, num);
if(res == 0)
printf("CONFLICT\n");
else if(res == 1)
printf("UNCERTAIN\n");
else
printf("OK\n");
}
}
return 0;
}
注释:该题原本的想法是,先使用并查集处理所有相等关系。然后每次取出当前所有入度为0的点,并判断这些点是否都在同一个集合里,必须是一一对应。如果有点入度为0但不在该集合中,那么应为不确定,如果某点在该集合中,但入度不为0,那么应为冲突。每次判断结束后,取所有这些点,将其后续点的入度减1,然后入度为0的点继续加入队列。这种方法一直没有过,还要再想一下。
四、图结构的判断
(一)判断是否连通 / 求连通分支数
无向图连通判断 —— 并查集
有向图连通判断 —— dfs或bfs
1. hdu 1272 小希的迷宫
题意:给出若干对结点,这些结点之间有无向边,求是否图中任两个结点间都有且仅有一条路可以到达。
思路:并查集。两结点间有且仅有一条路可以到达,使用并查集合并有边的两点,若合并时出现两点所在集合的根节点相同,说明两结点已在同一集合中,此时出现环,有超过一条路可以到达。如果并查集操作完毕后,有超过一个点的pre[i] == i,那么说明有超过一个连通分支,图中有两点之间无路可以到达。
该题通过并查集判连通分支数以及是否有无向环路。该题实际上是判断无向图是否是一颗树。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int pre[100005], rnk[100005], tar[100005], is_used[100005];
void init(void)
{
for(int i = 1; i <= 100000; ++ i)
{
pre[i] = i;
rnk[i] = 0;
}
}
int find_rt(int x)
{
if(pre[x] == x)
return x;
return pre[x] = find_rt(pre[x]);
}
int union_rnk(int x, int y)
{
int px = find_rt(x);
int py = find_rt(y);
if(px == py)
return 0;
if(rnk[px] < rnk[py])
pre[px] = py;
else
{
pre[py] = px;
if(rnk[px] == rnk[py])
rnk[px] ++;
}
return 1;
}
int main(void)
{
int u, v, nt, flag;
nt = 0;
flag = 1;
init();
memset(is_used, 0, sizeof(is_used));
memset(tar, 0, sizeof(tar));
while(scanf("%d%d", &u, &v) != EOF && u != -1 && v != -1)
{
if(u == 0 && v == 0)
{
int cnt = 0;
for(int i = 1; i <= nt; ++ i)
{
int tp = tar[i];
if(pre[tp] == tp)
cnt ++;
}
if(cnt > 1)
flag = 0;
if(flag == 0)
printf("No\n");
else
printf("Yes\n");
nt = 0;
flag = 1;
memset(is_used, 0, sizeof(is_used));
memset(tar, 0, sizeof(tar));
init();
}
else
{
if(is_used[u] == 0)
{
tar[++ nt] = u;
is_used[u] = 1;
}
if(is_used[v] == 0)
{
tar[++ nt] = v;
is_used[v] = 1;
}
if(union_rnk(u, v) == 0)
flag = 0;
}
}
return 0;
}
(二)强连通 / 求强连通分支数(有向图必有环)
强连通:如果两个顶点可以相互到达,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量。
求强连通分支的方法:tarjan算法 / kosaraju算法 tarjan算法效率更好
强连通两大算法讲解链接 两种算法讲解 (模板不用该链接中的,模板自己整理)另外一个详细的讲解
1. hdu 1269 迷宫城堡
题意:有向图,给出有向边的顶点对,求是否所有点全部强连通。
思路:判断整个图是否为强连通图,即强连通分量是否为1。
注意:(1)该题中可能存在n = 1, m = 0的数据。(2)第一次写时,cn为全局变量,但在函数内部不小心又重新定义了cn,导致错误,注意全局变量的使用。
方法1 —— kosaraju算法
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <stack>
#include <map>
#include <algorithm>
using namespace std;
vector<int> g1[10005], g2[10005];
int vis1[10005], vis2[10005], env[10005], cn = 0;
void dfs1(int x)
{
vis1[x] = 1;
for(int i = 0; i < g1[x].size(); ++ i)
{
int to = g1[x][i];
if(vis1[to] == 0)
{
vis1[to] = 1;
dfs1(to);
}
}
env[++ cn] = x;
}
void dfs2(int x)
{
vis2[x] = 1;
for(int i = 0; i < g2[x].size(); ++ i)
{
int to = g2[x][i];
if(vis2[to] == 0)
{
vis2[to] = 1;
dfs2(to);
}
}
}
int kosaraju(int n)
{
int cnt = 0;
cn = 0;
memset(vis1, 0, sizeof(vis1));
memset(vis2, 0, sizeof(vis2));
for(int i = 1; i <= n; ++ i)
{
if(vis1[i] == 0)
dfs1(i);
}
for(int i = cn; i >= 1; -- i)
{
if(vis2[env[i]] == 0)
{
dfs2(env[i]);
cnt ++;
}
}
return cnt;
}
int main(void)
{
int n, m, u, v;
while(scanf("%d%d", &n, &m) != EOF && n + m != 0)
{
for(int i = 1; i <= n; ++ i)
{
g1[i].clear();
g2[i].clear();
}
for(int i = 1; i <= m; ++ i)
{
scanf("%d%d", &u, &v);
g1[u].push_back(v);
g2[v].push_back(u);
}
int ans = kosaraju(n);
if(ans == 1)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
方法2 —— tarjan算法
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <stack>
#include <map>
#include <algorithm>
using namespace std;
vector<int> g[10005];
int instack[10005], dfn[10005], low[10005], belong[10005], cnt, index;
stack <int> s;
void init(int n)
{
memset(instack, 0, sizeof(instack));
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(belong, 0, sizeof(belong));
}
void tarjan(int x)
{
s.push(x);
instack[x] = 1;
dfn[x] = low[x] = ++ index;
for(int i = 0; i < g[x].size(); ++ i)
{
int t = g[x][i];
if(dfn[t] == 0)
{
tarjan(t);
low[x] = min(low[x], low[t]);
}
else if(instack[t] == 1)
low[x] = min(low[x], dfn[t]);
}
if(low[x] == dfn[x])
{
cnt += 1;
belong[x] = cnt;
int v = s.top();
while(v != x)
{
belong[v] = cnt;
instack[v] = 0;
s.pop();
v = s.top();
}
instack[x] = 0;
s.pop();
}
}
int main(void)
{
int n, m, u, v;
while(scanf("%d%d", &n, &m) != EOF && n + m != 0)
{
for(int i = 1; i <= n; ++ i)
g[i].clear();
for(int i = 1; i <= m; ++ i)
{
scanf("%d%d", &u, &v);
g[u].push_back(v);
}
while(s.empty() == 0)
s.pop();
index = 0;
cnt = 0;
init(n);
for(int i = 1; i <= n; ++ i)
{
if(dfn[i] == 0)
tarjan(i);
}
if(cnt == 1)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
有关强连通分量的各种应用,缩点等方法后续再详细整理。
(三)判断有向图结构是否为一条链或者一个环(一个环指整个图是一整个大环)
首先可以使用并查集预处理图中所有的连通块,如果连通块数量多于1个,那么其必不为上述两种情况。
1.一条链:上述判断通过后,要求图中只有一个点入度0出度1,只有一个点入度1出度0,其余点都必须入度1出度1。
2.一个环:上述判断通过后,要求图中所有点都必须入度1出度1(去掉重边后,如果不去重边则入出度相等)。
(四)判断图是否为树结构
1.无向图:如果一个无向图为树,则要求任意两点之间有且仅有一条路径可以到达。
方法:并查集。使用并查集合求连通分量数量,如果数量多于1那么有多个连通分量,不为树。在使用并查集过程中,如果某一时刻union的两个点所在集合的根结点相同,那么说明两结点已经在同一个集合中,此时图中出现环路。
2.有向图:如果一个有向图为树,需要满足两个条件。(1)仅有一个点入度为0,该点为根。(2)除根以外的所有点入度都为1(从根到其他点都有且仅有一条路到达)
有向图树的判断:hdu 1325 Is it a Tree?
题意:给出若干结点之间的边,判断当前图是否为树。
思路:并查集+入度判断。并查集判连通分量数,连通分量数大于1,则非树。然后在判断上述两个入度条件是否都成立,都成立则为树。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int pre[10005], rnk[10005], indeg[10005], is_used[10005], use[10005], pn;
void init(void)
{
for(int i = 1; i <= 10000; ++ i)
{
pre[i] = i;
rnk[i] = 0;
}
}
int find_rt(int x)
{
if(x == pre[x])
return x;
else
return pre[x] = find_rt(pre[x]);
}
int union_rnk(int x, int y)
{
int px = find_rt(x);
int py = find_rt(y);
if(px == py)
return 0;
if(rnk[px] < rnk[py])
pre[px] = py;
else
{
pre[py] = px;
if(rnk[px] == rnk[py])
rnk[px] += 1;
}
return 1;
}
int judge(void)
{
int cnt = 0;
for(int i = 1; i <= pn; ++ i)
{
if(pre[use[i]] == use[i])
cnt ++;
}
if(cnt != 1) return 0;
int cnt1 = 0, x = 0;
for(int i = 1; i <= pn; ++ i)
{
if(indeg[use[i]] == 0)
{
cnt1 += 1;
x = use[i];
}
}
if(cnt1 > 1) return 0;
for(int i = 1; i <= pn; ++ i)
{
if(x == use[i]) continue;
if(indeg[use[i]] > 1)
return 0;
}
return 1;
}
int main(void)
{
int u, v, tcase = 1;
init();
pn = 0;
memset(indeg, 0, sizeof(indeg));
memset(is_used, 0, sizeof(is_used));
memset(use, 0, sizeof(use));
while(scanf("%d%d", &u, &v) != EOF && u != -1 && v != -1)
{
if(u == 0 && v == 0)
{
int ans = judge();
if(ans == 1)
printf("Case %d is a tree.\n", tcase);
else
printf("Case %d is not a tree.\n", tcase);
memset(indeg, 0, sizeof(indeg));
memset(is_used, 0, sizeof(is_used));
memset(use, 0, sizeof(use));
init();
pn = 0;
tcase += 1;
}
else
{
if(is_used[u] == 0)
{
is_used[u] = 1;
use[++ pn] = u;
}
if(is_used[v] == 0)
{
is_used[v] = 1;
use[++ pn] = v;
}
union_rnk(u, v);
indeg[v] += 1;
}
}
return 0;
}
(五)判断是否为有向无环图
使用拓扑排序算法,在拓扑排序进行过程中,如果某一时刻图中不存在当前入度为0的点,那么存在图中存在回路。
注意:在判断图结构时需要对重边进行处理,同时要先判断连通分量的数量以确定其整个图是否连通,再进行后续判断。