D2. Construct an Array (Hard Version)
题目链接:Codeforces 2245D2
算法标签:图论、2-SAT 思想、贪心、构造
思路
把每条限制看成一条带类型的无向边:类型 1 要求两端之和非负,类型 2 要求两端之和为负。对于当前还没有处理的点,如果它只连接一种类型的边,就可以直接确定它的正负号:只剩类型 1 边就赋一个较大的正数,只剩类型 2 边就赋一个绝对值较大的负数。
用队列不断删除这样的点,并令最先删除的点绝对值最大,依次取 n,n-1,...,1。处理点 u 时,它的绝对值大于所有尚未处理的相邻点,因此它可以主导两数之和的符号:正数一定满足剩余的类型 1 边,负数一定满足剩余的类型 2 边。删点后更新邻点两类边的数量,新出现的可删点继续入队。
如果最后还有点没有删除,那么剩余图中每个点都同时连接两种边。取绝对值最大的点:若它为正,与它相连的类型 2 边要求出现绝对值更大的负数;若它为负,其类型 1 邻点必须是绝对值不小于它的正数,再沿这个正数的一条类型 2 边同样会要求更大的负数,均矛盾。因此此时无解。
时间复杂度为 O(n+m),空间复杂度为 O(n+m)。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, m;
cin >> n >> m;
vector<vector<int>> edge(n + 1);
vector<array<int, 2>> d(n + 1);
while (m--) {
int o, u, v;
cin >> o >> u >> v; o--;
edge[u].push_back(o ? -v : v);
edge[v].push_back(o ? -u : u);
d[u][o]++;
d[v][o]++;
}
vector<int> ans(n + 1);
vector<bool> vis(n + 1);
queue<int> q;
for (int u = 1; u <= n; u++) {
if (!d[u][0] || !d[u][1]) {
q.push(u);
vis[u] = true;
}
}
int cur = n;
while (!q.empty()) {
int u = q.front();
q.pop();
ans[u] = (d[u][0] ? 1 : -1) * cur--;
for (int v : edge[u]) {
int o = v < 0;
v = abs(v);
d[v][o]--;
if (!vis[v] && (!d[v][0] || !d[v][1])) {
q.push(v);
vis[v] = true;
}
}
}
if (cur) cout << "NO\n";
else {
cout << "YES\n";
for (int i = 1; i <= n; i++) cout << ans[i] << " \n"[i == n];
}
}
}
E. Tom and Jerry
题目链接:Codeforces 2245E
算法标签:博弈论、树形 DP、DFS、树
思路
这道博弈的关键结论是:第一步选择的路径为必胜路径,当且仅当路径的两个端点度数为奇数,所有内部点的度数为偶数。这个结论可以对后续游戏状态归纳得到;直观上,路径端点已经使用 1 条边、内部点已经使用 2 条边,所以恰好要求路径上每个点剩余的出边数都是偶数。
接下来只需统计这样的路径。将树以 1 为根,dp[u] 表示 u 的子树中,有多少个奇度点可以沿着一条内部全为偶度点的链连接到 u。
- 若
u的度数为奇数,u只能作为路径端点,不能作为内部点。每个儿子的dp[v]都能和u组成一条合法路径,所以加入答案,并令dp[u]=1。 - 若
u的度数为偶数,路径可以穿过u。从两个不同儿子子树中各取一个奇度端点即可组成合法路径,因此累加所有dp[v]的两两乘积;同时令dp[u]为所有儿子dp[v]之和,继续向父亲传递。
时间复杂度为 O(n),空间复杂度为 O(n)。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> edge;
vector<long long> dp;
long long ans;
void dfs(int u, int fa) {
long long sum = 0;
bool odd = edge[u].size() % 2;
for (int v : edge[u]) {
if (v == fa) {
continue;
}
dfs(v, u);
if (odd) {
ans += dp[v];
} else {
ans += dp[v] * sum;
sum += dp[v];
}
}
dp[u] = odd ? 1 : sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
edge.assign(n + 1, {});
dp.assign(n + 1, 0);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
ans = 0;
dfs(1, 0);
cout << ans << '\n';
}
}
F. Familiar?
题目链接:Codeforces 2245F
算法标签:区间 DP、笛卡尔树思想、组合数学、单调栈、计数
思路
处理一个区间 [l,r],设区间最小值出现在位置 k。执行到 k 时,它会把左半段最后留在单调栈中的元素全部弹出,所以 b[k] 就等于左区间处理结束后的栈大小。最小值入栈后不会再被右半段弹出,因此整个区间最终的栈大小等于右区间最终栈大小加一。
定义 dp[l][r][c] 表示区间 [l,r] 满足已知限制,并且最终栈大小为 c 的排列数;g[l][r] 表示不限制最终栈大小的排列数。枚举最小值位置 k:左边需要满足 a[k] 指定的栈大小,右边决定最终栈大小。除最小值外,还要从其余 r-l 个数中选择 k-l 个放到左边,因此乘上组合数 C(r-l,k-l)。
当 a[k]=-1 时,左边可以取任意栈大小,使用 g[l][k-1];否则使用 dp[l][k-1][a[k]]。计算 g 时右边使用 g[k+1][r],计算最终栈大小为 c 的 dp 时右边使用 dp[k+1][r][c-1]。代码令 a[n+1]=-1,所以最后答案就是 g[1][n]。
一次完整过程最多弹出 n-1 个元素,因此已知弹出次数之和若不小于 n 可以直接输出 0。这个判断也保证了代码实际开出的 dp 状态总数为 O(n^2)。总时间复杂度为 O(n^3),空间复杂度为 O(n^2)。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
int C[501][501];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
C[0][0] = 1;
for (int i = 1; i <= 500; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
}
}
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<int> a(n + 2);
int sum = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum += max(a[i], 0);
}
if (sum >= n) {
cout << 0 << '\n';
continue;
}
a[n + 1] = -1;
vector<vector<vector<int>>> dp(n + 2, vector<vector<int>>(n + 2));
vector<vector<int>> g(n + 2, vector<int>(n + 2));
for (int r = 1; r < n; r++) {
if (a[r + 1] != -1) {
for (int l = 1; l <= r; l++) {
dp[l][r].resize(a[r + 1] + 1);
}
}
}
for (int i = 1; i <= n; i++) {
if (a[i] <= 0) {
if (a[i + 1] == -1) {
g[i][i] = 1;
} else if (a[i + 1] > 0) {
dp[i][i][1] = 1;
}
}
}
for (int r = 2; r <= n; r++) {
for (int l = r - 1; l >= 1; l--) {
if (a[r + 1] == -1) {
// g[l][r] = sum(g[k+1][r] * C[r-l][k-1])
long long cur = 0;
for (int k = l; k <= r; k++) {
int left, right;
if (k != l && a[k] == -1) left = g[l][k - 1];
else if (k != l && a[k] != -1) left = dp[l][k - 1][a[k]];
else if (k == l) left = a[k] <= 0;
if (k != r) right = g[k + 1][r];
else if (k == r) right = 1;
cur += 1LL * C[r - l][k - l] * left % mod * right % mod;
}
g[l][r] = cur % mod;
} else {
// have a[r + 1]
// dp[l][r][c] = sum(dp[l][k-1][a[k]] * dp[k+1][r][c-1] * C[r-l][k-l])
for (int c = 1; c <= a[r + 1]; c++) {
long long cur = 0;
for (int k = l; k <= r; k++) {
int left, right;
if (k != l && a[k] == -1) left = g[l][k - 1];
else if (k != l && a[k] != -1) left = dp[l][k - 1][a[k]];
else if (k == l) left = a[k] <= 0;
if (k != r) right = dp[k + 1][r][c - 1];
else if (k == r) right = (c == 1);
cur += 1LL * C[r - l][k - l] * left % mod * right % mod;
}
dp[l][r][c] = cur % mod;
}
}
}
}
cout << g[1][n] << '\n';
}
}