洛谷题库 P2272 [ZJOI2007]最大半连通子图 题解与思路重现
有感而发,今晚一定先写篇题解。这可是我AC的第一道紫题!
题目中说半连通子图指的是对于图, 有 或者 。当时我对于“或”的理解是异或,也就是二者不能同时存在。
——那这题基本没法做啊?比如我们作一次DFS,搜索的时候光是找到一条横叉边,就已经让人头疼怎么处理;更别说还要判环,还要找“最大”的半连通子图。
但有一点是可以确定的——当图中可以将环处理掉时,“半连通子图”将会是一条链。易知对于链上的节点 (u是v的祖先),都有。
中午憋不住了,问了已经AC的lty大佬。他告诉我这是可兼或。果然!于是我们自然想到,对于每一个SCC(强连通分量),必然满足“半连通子图”的定义。
所以我们立刻使用Tarjan执行缩点。完成后将得到一张DAG(有向无环图),这时就可以通过拓扑排序,累加以其结尾的链的个数了。我自然写出一份爆零代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
#include <bits/stdc++.h> using namespace std; const int N = 102400, M = 1048576; int n, m, mod, u, v, head[N], headc[N]; struct edge { int v, nxt; } e[M]; struct c_edge { int v, w, nxt; } gc[M]; int st[N], dfn[N], low[N], top, num, c_cnt, c_e_cnt; int c[N], c_node_cnt[N], prev_vis; int sol_cnt[N], ind[N]; long long g_node_cnt[N], sol, max_g_cnt; bool ins[N], ex_vis[N], has_outd[N], vis[N]; queue <int> q; void tarjan (int x) { low[x] = dfn[x] = ++num; st[++top] = x, ins[x] = true; for (int i = head[x]; i; i = e[i].nxt) { if (!dfn[e[i].v]) { tarjan (e[i].v); low[x] = min (low[x], low[e[i].v]); } else if (ins[e[i].v]) low[x] = min (low[x], dfn[e[i].v]); } if (low[x] == dfn[x]) { ++c_cnt; int y; do { y = st[top--], ins[y] = false; c[y] = c_cnt, ++c_node_cnt[c_cnt]; } while (x != y); } } void add_c (int x, int y, int w) { gc[++c_e_cnt].v = y, gc[c_e_cnt].nxt = headc[x], gc[c_e_cnt].w = w, headc[x] = c_e_cnt; } void topo () { q.push(0); while (!q.empty()) { int x = q.front(); q.pop(); for (int i = headc[x]; i; i = gc[i].nxt) { sol_cnt[gc[i].v] = (sol_cnt[gc[i].v] + sol_cnt[x]) % mod; if (--ind[gc[i].v] == 0) q.push(gc[i].v); } } } void bfs () { q.push(0); while (!q.empty()) { int x = q.front(); q.pop(); vis[x] = false; for (int i = headc[x]; i; i = gc[i].nxt) { g_node_cnt[gc[i].v] = max(g_node_cnt[gc[i].v], g_node_cnt[x] + gc[i].w); if (!vis[gc[i].v]) q.push(gc[i].v); } } } int main () { /* 洛谷题库 P2272 [ZJOI2007]最大半连通子图 吴秋实 */ scanf ("%d%d%d", &n, &m, &mod); for (int i = 1; i <= m; ++i) { scanf ("%d%d", &u, &v); e[i].nxt = head[u], e[i].v = v, head[u] = i; } // 缩点 for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan (i); for (int i = 1; i <= n; ++i) for (int j = head[i]; j; j = e[j].nxt) { if (c[e[i].v] == c[i]) continue; add_c (c[i], c[e[i].v], c_node_cnt[c[e[i].v]]); ++ind[c[e[i].v]], has_outd[c[i]] = true; } for (int i = 1; i <= c_cnt; ++i) if (ind[i] == 0) { add_c (0, i, c_node_cnt[i]); ind[i] = 1; } // 去除重边 for (int i = 1; i <= c_cnt; ++i) { memset (ex_vis, 0, sizeof ex_vis); prev_vis = 0; for (int j = headc[i]; j; j = gc[j].nxt) { if (ex_vis[gc[j].v]) gc[prev_vis].nxt = gc[j].nxt, --ind[gc[j].v]; else ex_vis[gc[j].v] = true; prev_vis = j; } } // 作拓扑排序 sol_cnt[0] = 1; topo (); // BFS寻找最大解 bfs (); for (int i = 1; i <= c_cnt; ++i) { if (!has_outd[i]) sol = (sol + sol_cnt[i]) % mod; max_g_cnt = max(max_g_cnt, g_node_cnt[i]); } printf("%lld\n%lld", max_g_cnt, sol); return 0; }
又是TLE又是MLE又是WA的。
仔细一看,存在很大问题:
- 先进行一次拓扑排序找“方案数”,又用BFS找最大解。而且为了不漏解,BFS遍历了每一条可能的路径(甚至不是按照节点层数扩展的)(粗算时间复杂度是,即每个点的出度的乘积),显然直接爆时间;
- 建缩点过后新图的代码写错了(细节啊!)。
将这些细枝末节的地方改正之后,再交,仍然爆零。并且查看输出信息发现,第一行(输出最大链长度)没有问题,但第二行(输出方案数)全部不正确。
冥思苦想了好久……修改了一些细节提交,仍然不正确。无奈,只得去题解板块寻找与我的相同的思路。但令人惊奇的是,这些题解求答案时,无一例外都有这个转移方程:
设以编号为的SCC为结尾的最长链的条数为,长度为,有编号为的SCC,,则有
为什么待扩展方案链长大于现有方案链长时才发生转移呢?我们再来读题——“若 是 所有半连通子图中包含节点数最多的,则称是的最大半连通子图。”
看来连题都读错了。它求的是“最长链数量”,而我求的是“以第个SCC结尾的链的总方案数”。这样一来,转移方程也就可以理解了。现有方案更优,则直接按照现有方案信息转移;若同样优秀,则方案数累加。
我们再倒回去说说使用拓扑排序求解、转移答案的正确性。因为在拓扑排序中会记录每个点的入度,每遍历一条边都会将入度减,当且仅当时才会将入队,扩展节点。则易知这样可以保证每一个节点从入边获得最优解。
(哦对了,拓扑之前要去重边。)
时间复杂度。AC代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
#include <bits/stdc++.h> using namespace std; const int N = 409600, M = 4096000; int n, m, mod, u, v, head[N], headc[N]; struct edge { int v, nxt; } e[M]; struct c_edge { int v, w, nxt; } gc[M]; int st[N], dfn[N], low[N], top, num, c_cnt, c_e_cnt; int c[N], c_node_cnt[N], prev_vis; int sol_cnt[N], ind[N]; long long g_node_cnt[N], sol, max_g_cnt; bool ins[N], ex_vis[N], has_outd[N], vis[N]; queue q; void tarjan (int x) { low[x] = dfn[x] = ++num; st[++top] = x, ins[x] = true; for (int i = head[x]; i; i = e[i].nxt) { if (!dfn[e[i].v]) { tarjan (e[i].v); low[x] = min (low[x], low[e[i].v]); } else if (ins[e[i].v]) low[x] = min (low[x], dfn[e[i].v]); } if (low[x] == dfn[x]) { ++c_cnt; int y; do { y = st[top--], ins[y] = false; c[y] = c_cnt, ++c_node_cnt[c_cnt]; } while (x != y); } } void add_c (int x, int y, int w) { gc[++c_e_cnt].v = y, gc[c_e_cnt].nxt = headc[x], gc[c_e_cnt].w = w, headc[x] = c_e_cnt; } void topo () { q.push(0); while (!q.empty()) { int x = q.front(); q.pop(); memset (ex_vis, 0, sizeof ex_vis); for (int i = headc[x]; i; i = gc[i].nxt) { if (!ex_vis[gc[i].v]) { /* 题目中“最大半联通子图”的意思并不是单纯的“不同的”链的“方案数”叠加。 */ /* 细读它的定义,应该是有两条可能的链延展到同一个节点时,取节点数“最大”的一个,才是最大半连通子图。 */ /* 那么,链长相等时,就应该累加方案数。这才是正确的转移方式。 */ if (g_node_cnt[x] + gc[i].w > g_node_cnt[gc[i].v]) // 更好的方案!采取它进行转移。 sol_cnt[gc[i].v] = sol_cnt[x], g_node_cnt[gc[i].v] = g_node_cnt[x] + gc[i].w; else if (g_node_cnt[x] + gc[i].w == g_node_cnt[gc[i].v]) // 二者相等,将它们的前序方案累加。 sol_cnt[gc[i].v] = (sol_cnt[gc[i].v] % mod + sol_cnt[x] % mod) % mod; ex_vis[gc[i].v] = true; } if (--ind[gc[i].v] == 0) q.push(gc[i].v); } } } int main () { /* 洛谷题库 P2272 [ZJOI2007]最大半连通子图 吴秋实 */ scanf ("%d%d%d", &n, &m, &mod); for (int i = 1; i <= m; ++i) { scanf ("%d%d", &u, &v); e[i].nxt = head[u], e[i].v = v, head[u] = i; } // 缩点 for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan (i); for (int i = 1; i <= n; ++i) for (int j = head[i]; j; j = e[j].nxt) { if (c[e[j].v] == c[i]) continue; add_c (c[i], c[e[j].v], c_node_cnt[c[e[j].v]]); ++ind[c[e[j].v]], has_outd[c[i]] = true; // 细节!细节!!细节!!! } for (int i = 1; i <= c_cnt; ++i) if (ind[i] == 0) { add_c (0, i, c_node_cnt[i]); ind[i] = 1; } // 作拓扑排序 sol_cnt[0] = 1; topo (); for (int i = 1; i <= c_cnt; ++i) { // 统计方案,找最大解 if (!has_outd[i]) if (max_g_cnt < g_node_cnt[i]) // 现在找到的才是“最大” sol = sol_cnt[i] % mod, max_g_cnt = g_node_cnt[i]; else if (max_g_cnt == g_node_cnt[i]) // 或者二者相等?累加方案数。 sol = (sol_cnt[i] + sol) % mod; } cout << max_g_cnt << endl << sol; return 0; }
再次感叹Tarjan老先生算法的奇妙。