把出现在限制边中的球员和 1 号球员单独作为特殊点,其余球员合并成一个普通组,做 m 轮压缩 DP。
OJ: luogu
题目 ID: P5888
难度:普及+/提高
标签:动态规划图论dp
日期: 2026-06-19 13:31
题意
有
一共传
要求统计第
思路
先看小数据下最直接的做法:
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:小数据直接在完整图上做 m 轮 DP,用来帮助理解和辅助对拍。
const int MOD = 998244353;
int n, m, k;
bool ban_edge[205][205];
int dp[205], ndp[205];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i <= k; i++) {
int a, b;
cin >> a >> b;
if (a != b) {
ban_edge[a][b] = true;
}
}
dp[1] = 1;
for (int step = 1; step <= m; step++) {
for (int i = 1; i <= n; i++) {
ndp[i] = 0;
}
for (int u = 1; u <= n; u++) {
if (dp[u] == 0) {
continue;
}
for (int v = 1; v <= n; v++) {
if (u == v || ban_edge[u][v]) {
continue;
}
ndp[v] += dp[u];
if (ndp[v] >= MOD) {
ndp[v] -= MOD;
}
}
}
for (int i = 1; i <= n; i++) {
dp[i] = ndp[i];
}
}
cout << dp[1] << '\n';
return 0;
}下面是另一种「状态搜索」风格的暴力写法。它把状态写成"已经传了几次、球在谁手中",递归枚举下一轮传给哪个球员,重复状态用记忆化保存:
另一种暴力写法:状态搜索
// brute_01_style.cpp:状态搜索风格暴力,把“传了几次、球在谁手中”作为状态。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
const int MAXM = 205;
const int MOD = 998244353;
int n, m, k;
bool ban_edge[MAXN][MAXN];
int memo[MAXM][MAXN];
bool vis[MAXM][MAXN];
// dfs_pass(step, u):已经传了 step 次,球在 u 手中,继续枚举下一轮传给谁。
int dfs_pass(int step, int u) {
if (step == m) {
return u == 1;
}
if (vis[step][u]) {
return memo[step][u];
}
vis[step][u] = true;
int ways = 0;
for (int v = 1; v <= n; v++) {
if (v == u || ban_edge[u][v]) {
continue;
}
ways += dfs_pass(step + 1, v);
if (ways >= MOD) {
ways -= MOD;
}
}
memo[step][u] = ways;
return ways;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i <= k; i++) {
int a, b;
cin >> a >> b;
if (a != b) {
ban_edge[a][b] = true;
}
}
cout << dfs_pass(0, 1) << '\n';
return 0;
}brute.cpp 就是在完整图上做普通 DP:
这个思路没问题,但
关键观察是:虽然球员总数很多,但真正"特殊"的球员只有两类:
- 出现在某条非自环限制边里的球员
号球员
把这些点单独拿出来,剩下的点都叫"普通点"。
因为普通点:
- 没有额外禁止入边
- 也没有额外禁止出边
所以任意一步后,所有普通点的方案数一定都相同,可以合并成一个状态。
状态表
这张表展示压缩后的状态含义:
| 状态 | 含义 |
|---|---|
| 当前在第 |
|
| 当前在任意一个普通点的方案数 |
若当前总方案数和是
- 转移到普通点
普通点没有额外禁止入边,只需要去掉"自己传给自己"这一种情况:
- 转移到特殊点
先去掉
再减掉所有被额外禁止传向
也就是:
这样每一轮只需要维护:
- 所有特殊点
- 一个普通组
就能完成转移。
DP 公式
设当前所有状态方案数总和为
转移到特殊点
每轮按上式更新,传完
公式解释:普通点完全等价,因此每个普通点可以共用一个方案数。转移到某点时,先从总方案数中去掉自己传给自己的非法情况;若目标是特殊点,还要减去题目额外禁止的来源。
代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
int n, m, k;
vector<int> ids;
vector<pair<int, int> > edges;
vector<vector<int> > in_from; // in_from[v]:哪些特殊点不能把球传给 v
vector<long long> cur_sp, nxt_sp;
int ordinary_cnt;
int start_idx;
long long cur_ordinary, nxt_ordinary;
int get_id(int x) {
return lower_bound(ids.begin(), ids.end(), x) - ids.begin();
}
int norm(long long x) {
x %= MOD;
if (x < 0) {
x += MOD;
}
return (int)x;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
ids.push_back(1);
for (int i = 1; i <= k; i++) {
int a, b;
cin >> a >> b;
if (a == b) {
// 自己传给自己本来就不允许,这种限制没有额外影响。
continue;
}
edges.push_back(make_pair(a, b));
ids.push_back(a);
ids.push_back(b);
}
sort(ids.begin(), ids.end());
ids.erase(unique(ids.begin(), ids.end()), ids.end());
sort(edges.begin(), edges.end());
edges.erase(unique(edges.begin(), edges.end()), edges.end());
int sp_cnt = (int)ids.size();
ordinary_cnt = n - sp_cnt;
start_idx = get_id(1);
in_from.assign(sp_cnt, vector<int>());
for (int i = 0; i < (int)edges.size(); i++) {
int u = get_id(edges[i].first);
int v = get_id(edges[i].second);
in_from[v].push_back(u);
}
cur_sp.assign(sp_cnt, 0);
nxt_sp.assign(sp_cnt, 0);
cur_sp[start_idx] = 1;
cur_ordinary = 0;
for (int step = 1; step <= m; step++) {
long long total = 1LL * ordinary_cnt * cur_ordinary % MOD;
for (int i = 0; i < sp_cnt; i++) {
total += cur_sp[i];
if (total >= MOD) {
total -= MOD;
}
}
for (int v = 0; v < sp_cnt; v++) {
long long val = total - cur_sp[v];
for (int i = 0; i < (int)in_from[v].size(); i++) {
int u = in_from[v][i];
val -= cur_sp[u];
}
nxt_sp[v] = norm(val);
}
nxt_ordinary = norm(total - cur_ordinary);
cur_sp.swap(nxt_sp);
cur_ordinary = nxt_ordinary;
}
cout << cur_sp[start_idx] % MOD << '\n';
return 0;
}复杂度
- 时间复杂度:
,其中 是特殊点个数,且 - 空间复杂度:
总结
这题的关键不是传球本身,而是看出:
- 大多数球员完全等价,可以合并成一个统一状态
一旦完成这一步压缩,原本
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
