CF1425B Blue and Red of Our Faculty!
CF1425B Blue and Red of Our Faculty!
题目来源:Codeforces, 2020 ICPC, COMPFEST 12, Indonesia Multi-Provincial Contest (Unrated, Online Mirror, ICPC Rules, Teams Preferred), CF1425B Blue and Red of Our Faculty!
题目大意
给定一张 \(n\) 个点 \(m\) 条边的无向连通图,保证无自环,保证除节点 \(1\) 外每个点的度数都为 \(2\)。
有两人 Red 和 Blue 同时从节点 \(1\) 出发。初始时所有边都是灰色。Red 每经过一条边就会将它染成红色,Blue 每经过一条边就会将它染成蓝色。每轮中,每人会选择一条与当前所在节点相连的、灰色的边,并走到边的另一端。同一轮里两人选择的边不能相同。
当无法进行下一轮时,整个过程停止。问两人能走出多少种不同的最终局面,答案对 \(10^9 + 7\) 取模。两个最终局面不同当且仅当存在一条边,在最终局面下颜色不同。
数据范围:\(1\leq n\leq 2000\),\(1\leq m\leq 2n\)。
本题题解
根据“保证除节点 \(1\) 外每个点的度数都为 \(2\)”,可以发现整张图呈一个花瓣状。也就是有若干个环,节点 \(1\) 是它们的公共点。我们可以用一个序列(或者说可重集)\(\{a_1,a_2,\dots,a_k\}\) 来描述整张图,其中 \(a_i\) 表示第 \(i\) 个环的大小,\(k\) 是环的数量。
考虑结束时两人的位置。可以分为三种情况:
- 两人都在节点 \(1\)。此时他们一定经过了所有环(图中没有灰边),且步数相同。方案数就相当于把 \(a\) 划分为两个集合,使他们和相等的方案数。
- 两人都在某个环的某个节点上(不含节点 \(1\))。设该环的大小为 \(l\),在环上 Red 走了 \(x\) (\(1\leq x < l\)) 步,则 Blue 走了 \(l - x\) 步。两人的步数差为 \((l - x) - x = l - 2x\)。问题相当于从 \(a\) 除当前环外的部分里(即 \(a\setminus\{l\}\)),选出两个互不相交的子集,使他们的和之差为 \(l - 2x\)。
- 两人都在某个环上,且相距为 \(1\)(隔着一条边)。此时又可以分为两种情况:
- 两人都不在节点 \(1\) 上(严格位于环上)。这与情况 2 类似,只不过此时两人的步数差为 \(l - 1 - 2x\),且 \(1\leq x\leq l - 2\)。
- 其中一人在节点 \(1\) 上。此时 \(x = 0\) 或 \(x = l - 1\)。除了两人的步数差为 \(l - 1 - 2x\) 外,还需要保证其他环都被走过。也就是选出的两个子集的并为全集(全集,指:\(a\) 除去一个 \(l\) 外的其他所有元素,即 \(a\setminus\{l\}\))。
这三种情况,都可以用同一个 DP 来计算。
先枚举 \(l\),从 \(a\) 中暂时删掉一个 \(l\)(如果是计算情况 \(1\),则不必删除任何元素)。
设 \(\text{dp}(i,j,\text{flag})\) 表示考虑了前 \(i\) 个环,当前 【Red 走的步数】\(-\)【Blue 走的步数】等于 \(j\)。\(\text{flag}\) 是个 \(0/1\) 值,表示有没有两人都没走过的环。注意 \(j\) 这里,把“朴素地记录两人步数分别是多少”,变成“记录两者之差”,是个设计状态的小套路(CSP2019 d2t1 也有用到)。这么做是因为我们最终只关心它们的差。
转移就考虑当前环,是被 Red 走,还是被 Blue 走,还是两人都不走:
情况 1 的答案是 \(\text{dp}(k, 0, 1)\)。\(k\) 是环的数量。
情况 2 的答案是 \(\displaystyle 2\times \sum_{x = 1}^{l - 1}(\text{dp}(k - 1, l - 2x, 0) + \text{dp}(k - 1, l - 2x, 1))\)。其中乘以 \(2\) 是因为最后一个环,Red 可以从两个方向(顺时针 / 逆时针)之一开始走。
情况 3 的答案是 \(\displaystyle 2\times \left(\sum_{x = 1}^{l - 2}\text{dp}(k - 1, l - 1 - 2x,0) + \sum_{x = 0}^{l - 1}\text{dp}(k - 1, l - 1 - 2x,1)\right)\)。
一次 DP 的时间复杂度是 \(\mathcal{O}(n^2)\)。但是外层要枚举 \(l\)。虽然环的数量可能高达 \(\mathcal{O}(n)\) 个,但由于 \(\sum\text{环长} = \mathcal{O}(n)\),所以不同的环长 \(l\) 只有 \(\mathcal{O}(\sqrt{n})\) 种。暴力枚举 \(l\) 再做 DP 的总时间复杂度 \(\mathcal{O}(n^2\sqrt{n})\),可以通过本题。另有一些优化方法:
- 优化方法1:这是一个缺一背包的模型,所以可以用分治来优化,做到 \(\mathcal{O}(n^2\log n)\)。
- 优化方法2:仍然暴力枚举 \(l\)。不对可重集 \(a\) 做 DP,而是把 \(a\) 缩成若干个二元组 \((\text{元素},\text{出现次数})\),然后类似于做多重背包。单次 DP 的时间复杂度是 \(\mathcal{O}(n\sqrt{n})\),总时间复杂度 \(\mathcal{O}(n^2)\)。
参考代码
时间复杂度 \(\mathcal{O}(n^2\sqrt{n})\)。
// problem: CF1425B
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 2000;
const int MOD = 1e9 + 7;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
int n, m;
vector<int> G[MAXN + 5];
int cir[MAXN + 5];
int a[MAXN + 5], cnt;
int dp[MAXN + 5][MAXN * 4 + 5][2];
int calc_dp(int len) {
// 求两人最终在一个长度为 len 的环上相遇的方案数
// 特别地, len = 0 表示把所有环都完整地走一遍
cnt = 0;
int sum = 0;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= cir[i] - (i == len); ++j) {
a[++cnt] = i;
sum += i;
}
}
for (int i = 0; i <= cnt; ++i) {
for (int j = -sum; j <= sum; ++j) {
for (int k = 0; k <= 1; ++k) {
dp[i][j + sum][k] = 0;
}
}
}
dp[0][0 + sum][1] = 1;
for (int i = 1; i <= cnt; ++i) {
for (int j = -sum; j <= sum; ++j) {
for (int k = 0; k <= 1; ++k) if (dp[i - 1][j + sum][k]) {
// red
assert(j + a[i] <= sum);
add(dp[i][j + a[i] + sum][k], dp[i - 1][j + sum][k]);
// blue
assert(j - a[i] >= -sum);
add(dp[i][j - a[i] + sum][k], dp[i - 1][j + sum][k]);
// neither
add(dp[i][j + sum][0], dp[i - 1][j + sum][k]);
}
}
}
if (!len) {
return dp[cnt][0 + sum][1];
}
int res = 0;
for (int i = 1; i < len; ++i) {
// red 走的
int dif = (len - i) - i;
if (abs(dif) > sum)
continue;
add(res, dp[cnt][dif + sum][0]);
add(res, dp[cnt][dif + sum][1]);
}
for (int i = 0; i < len; ++i) {
int dif = (len - i - 1) - i;
if (abs(dif) > sum)
continue;
if (i != 0 && i != len - 1)
add(res, dp[cnt][dif + sum][0]);
add(res, dp[cnt][dif + sum][1]);
}
res = res * 2 % MOD;
return res;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
static bool vis[MAXN + 5];
for (int i = 0; i < SZ(G[1]); ++i) {
int u = G[1][i];
if (vis[u]) {
continue;
}
int len = 1;
int lst = 1;
while (u != 1) {
len++;
vis[u] = 1;
int nxt_u = (G[u][0] ^ G[u][1] ^ lst);
lst = u;
u = nxt_u;
}
cir[len]++;
// cerr << "found cir of len: " << len << endl;
}
int ans = 0;
for (int i = 2; i <= n; ++i) {
if (cir[i]) {
add(ans, (ll)calc_dp(i) * cir[i] % MOD);
}
}
add(ans, calc_dp(0));
cout << ans << endl;
return 0;
}