H - 流画溢彩
基本信息
题目出处 | 2023 广东省大学生程序设计竞赛 |
队伍通过率 | 6/295 (2.0%) |
题解
本题 idea 来自同名桌游《流画溢彩》。
因为后面的操作会覆盖前面的操作,思考起来比较麻烦。我们不妨把操作顺序倒过来,这样一旦某一列的值确定了,后续操作就再也不会更改这一列的值。以下题解中操作的“先后”,是按操作顺序逆转之后的“先后”来说的。
首先,如果某个操作把两个数都改成 \(2\),那么这个操作肯定最优,最先考虑;相应地,如果某个操作把两个数都改成 \(1\),那么这个操作肯定最差,最后考虑。剩下的就是一个数改成 \(1\),一个数改成 \(2\) 的操作。
本题的关键在于把题目转化为图论问题。把每一列看成一个点,把每个操作从改成 \(1\) 的那一列向改成 \(2\) 的那一列连一条有向边。
考虑选择一条边 \(u \to v\),进行它代表的操作。此时 \(a_u\) 的值将被锁定为 \(1\),而 \(u\) 能直接或间接到达的所有点的值将被锁定为 \(2\)(只要从 \(u\) 出发,按 dfs 序走一遍 \(u\) 能到达的边即可)。我们要做的就是让锁定为 \(1\) 的列尽量少,也就是选择尽量少的点,让它们能到达的点的并集等于整张图的点集。
容易发现,将强连通分量缩点以后,我们将得到一个有向无环图。有向无环图上每一个入度为 \(0\) 的点我们都必须选择,才能让它们本身以及它们的后继覆盖整张图。也就是说,我们每次从一个入度为 \(0\) 的强连通分量中选择一个点,按 dfs 顺序输出边的编号即可。
这里有一个细节:如果一个入度为 \(0\) 的强连通分量被一个 \((l_i, 2, r_i, 2)\) 操作所影响,那么应该选择被影响的点为 dfs 的起始点,因为被影响的点代表的列已经被锁定为 \(2\)。
复杂度 \(\mathcal{O}(n + m)\)。
参考代码
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
118
119
120
121
122
123
124
125
126
127 | #include <bits/stdc++.h>
#define MAXN ((int) 5e5)
#define MAXM ((int) 5e5)
using namespace std;
int n, m, OP[MAXM + 10][4];
vector<int> ans;
vector<int> e[MAXN + 10], v[MAXN + 10];
// bel[i]:点 i 属于哪个强连通分量
int clk, bCnt, dfn[MAXN + 10], low[MAXN + 10], bel[MAXN + 10];
bool inStk[MAXN + 10];
stack<int> stk;
// deg[i]:强连通分量 i 的入度
int deg[MAXN + 10];
// vis[i]:dfs 构造答案的过程中,节点 i 是否被访问过
bool vis[MAXN + 10];
// tarjan 求强连通分量
void tarjan(int sn) {
low[sn] = dfn[sn] = ++clk;
stk.push(sn); inStk[sn] = true;
for (int fn : e[sn]) {
if (!dfn[fn]) {
tarjan(fn);
low[sn] = min(low[sn], low[fn]);
} else if (inStk[fn]) {
low[sn] = min(low[sn], dfn[fn]);
}
}
if (dfn[sn] == low[sn]) {
++bCnt;
while (stk.top() != sn) {
bel[stk.top()] = bCnt;
inStk[stk.top()] = false;
stk.pop();
}
bel[stk.top()] = bCnt;
inStk[stk.top()] = false;
stk.pop();
}
}
// 从节点 sn 开始 dfs,并按 dfs 序将访问过的每条边加入 vec 里
void dfs(int sn, vector<int> &vec) {
vis[sn] = true;
for (int i = 0; i < e[sn].size(); i++) {
int fn = e[sn][i];
int val = v[sn][i];
vec.push_back(val);
if (!vis[fn]) dfs(fn, vec);
}
}
void solve() {
scanf("%d%d", &n, &m);
vector<int> one, two;
for (int i = 1; i <= n; i++) e[i].clear(), v[i].clear();
for (int i = 1; i <= m; i++) {
for (int j = 0; j < 4; j++) scanf("%d", &OP[i][j]);
if (OP[i][1] == 1 && OP[i][3] == 1) {
// 两个数都改成 1,最差的操作
one.push_back(i);
} else if (OP[i][1] == 2 && OP[i][3] == 2) {
// 两个数都改成 2,最好的操作
two.push_back(i);
} else if (OP[i][1] == 1) {
// 图中加一条从 1 指向 2 的边
e[OP[i][0]].push_back(OP[i][2]);
v[OP[i][0]].push_back(i);
} else {
// 图中加一条从 1 指向 2 的边
e[OP[i][2]].push_back(OP[i][0]);
v[OP[i][2]].push_back(i);
}
}
// 顺序输出的答案中,两个数都改成 1 的最差操作最先输出
ans.clear();
for (int x : one) ans.push_back(x);
memset(dfn, 0, sizeof(int) * (n + 3));
clk = bCnt = 0;
for (int sn = 1; sn <= n; sn++) if (!dfn[sn]) tarjan(sn);
memset(deg, 0, sizeof(int) * (n + 3));
for (int sn = 1; sn <= n; sn++) {
for (int fn : e[sn]) if (bel[sn] != bel[fn]) deg[bel[fn]]++;
}
vector<int> vec;
memset(vis, 0, sizeof(bool) * (n + 3));
// 如果一个入度为 0 的强连通分量受一个 (l_i, 2, r_i, 2) 操作影响,那么需要从 l_i 或 r_i 开始 dfs
for (int x : two) for (int j = 0; j < 4; j += 2) {
int sn = OP[x][j];
if (deg[bel[sn]] == 0 && !vis[sn]) dfs(sn, vec);
}
// 剩下的入度为 0 的强连通分量,随便找一个点开始 dfs
for (int sn = 1; sn <= n; sn++) {
if (deg[bel[sn]] == 0 && !vis[sn]) dfs(sn, vec);
}
// dfs 序是答案的倒序
reverse(vec.begin(), vec.end());
ans.insert(ans.end(), vec.begin(), vec.end());
// 顺序输出的答案中,两个数都改成 2 的最好的操作最后输出
for (int x : two) ans.push_back(x);
// 计算序列最终的和
unordered_map<int, int> mp;
for (int x : ans) {
mp[OP[x][0]] = OP[x][1];
mp[OP[x][2]] = OP[x][3];
}
int tot = 0;
for (auto it = mp.begin(); it != mp.end(); it++) tot += it->second;
printf("%d\n", tot);
for (int i = 0; i < m; i++) printf("%d%c", ans[i], "\n "[i + 1 < m]);
}
int main() {
int tcase; scanf("%d", &tcase);
while (tcase--) solve();
return 0;
}
|