把每个小朋友只依赖的连续 5 个围栏压成 32 种窗口状态,滑动时右移补位并在窗口起点结算得分,最后枚举初窗口的 32 种取值破环取最大。
OJ: roj
题目 ID: 1596
难度:提高
标签:动态规划状态压缩环形DPpython
创建: 2026-09-30 20:56
更新: 2026-10-07 13:50
形式化题目
有
即「至少一个害怕的动物被移走」或「至少一个喜欢的动物被保留」。求高兴人数最大可能的取值。
其中
以样例 1 的第三个小朋友(
正解
思路
朴素做法及其瓶颈。 最直接的想法是枚举
把连续 5 个围栏压成状态。 既然任何时刻只有 5 个围栏的取值会影响未来,那就只记住它们。
定义窗口状态
窗口沿环前进一步时,起点处的那个围栏离开窗口,新围栏进入窗口末尾。用位运算描述:
旧位 s >> 1)和保留(s >> 1 | 16,16 = 1 << 4)。
把得分挂在窗口起点上。 一个小朋友的 5 个围栏,在窗口起点恰好等于他的
左边是「有害怕的动物被移走」(
以样例 1 的前四个窗口起点为例,表中位串从左到右是位
| 窗口起点 |
该起点小朋友的 afraid / fond |
得 1 分的状态数 | 得 0 分的状态 |
|---|---|---|---|
| 1 | 无 | 0 | 全部 32 个 |
| 2 | 00100 / 10001 |
28 | 00100 00110 01100 01110 |
| 3 | 01000 / 00010 |
24 | 01000 01001 01100 01101 11000 11001 11100 11101 |
| 4 | 无 | 0 | 全部 32 个 |
观察重点:得 0 分的状态恰好是「害怕的全留下、喜欢的全移走」的组合。起点 ??1?0 且位 4 为 0、位 0 为 0
的 4 个状态让他不高兴——afraid & ~s 与 fond & s 同时为 0。起点 1 和 4 没有小朋友,
32 个状态一律得 0 分。
沿环递推。 设
其中
破环。 环上没有天然的「第一个围栏」。处理办法是枚举第 1 个窗口(围栏 first,把环拆成 32 条链:
- 初值
,其余状态为 ; - 窗口起点依次推到
,每步按上面的递推更新; - 再做一次不带得分的平移,窗口回到起点
(此时覆盖围栏 ,模 后 正是 ); - 只有
才代表首尾吻合的合法环形安排,取 32 次枚举的最大值即答案。
第 3 步不计分,是因为起点 1 的那批小朋友在第 1 步已经算过;代码里给 gain 末尾追加一行
全
样例 1 取 first = 00000(围栏
终值
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 21:05
# update_at: 2026-09-30 21:05
import sys
WINDOW = 5
STATES = 1 << WINDOW # 窗口状态数:5 个围栏各「留 / 移」两种,共 32 种
NEG = -1 << 60 # 不可达状态的初值,比任何合法答案都小
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
fence = next(data)
kids = next(data)
# 按小朋友看到的起始围栏 E 分桶,桶里存 (害怕集合, 喜欢集合) 的 5 位掩码,
# 位 j 表示他视野里第 j 个围栏(即编号 E+j),1 表示该围栏出现在对应名单里。
by_start: list[list[tuple[int, int]]] = [[] for _ in range(fence + 1)]
for _ in range(kids):
start = next(data)
feared = next(data)
liked = next(data)
# 围栏编号绕圈回绕:(编号 - E) mod N 就是它在窗口内的下标
afraid = 0
for _ in range(feared):
afraid |= 1 << ((next(data) - start) % fence)
fond = 0
for _ in range(liked):
fond |= 1 << ((next(data) - start) % fence)
by_start[start].append((afraid, fond))
# gain[e][s]:起始围栏为 e、5 位保留情况为 s 时,能高兴的小朋友数。
# 高兴 <=> 有害怕的动物被移走 (afraid & ~s) 或 有喜欢的动物被留下 (fond & s)。
# 位 j 对齐,故 s 可直接与两个掩码按位运算。
# 末尾多出一行全 0:窗口滑到起点 N+1(就是起点 1)时只做平移、不重复计分。
gain = [
[sum(1 for afraid, fond in by_start[e] if afraid & ~s or fond & s) for s in range(STATES)]
for e in range(1, fence + 1)
] + [[0] * STATES]
# next_s: 窗口状态从 s 滑动一格后的两个候选(新围栏的动物留 / 移)。
# 位 j 记录围栏 pos+j,滑动后旧位 j 变成新位 j-1,新位 4 就是刚进视野的围栏。
next_s = [(s >> 1, s >> 1 | 16) for s in range(STATES)]
best = 0
# 圆环没有天然起点,只能枚举第 1 个窗口的 5 位状态;滑 N 格后必须回到同一个状态。
for first in range(STATES):
cur = [NEG] * STATES
cur[first] = gain[0][first] # gain[0] 对应起始围栏 1
# 起点 1 已作为初值算过,再滑 N 次:前 N-1 次补齐起点 2..N,最后一次不带分回到起点 1。
for row in gain[1:]:
nxt = [NEG] * STATES
for s, val in enumerate(cur):
if val == NEG:
continue
for t in next_s[s]:
cand = val + row[t]
if cand > nxt[t]:
nxt[t] = cand
cur = nxt
if cur[first] > best: # 绕回起点,只有状态吻合的方案才是合法的圆环安排
best = cur[first]
print(best)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 11:52
* update_at: 2026-10-05 11:52
*/
// main.cpp:环形状态压缩 DP 求解最多高兴的小朋友数。
#include <iostream>
using namespace std;
typedef long long ll;
const int MAXN = 10005; // 围栏数上限
const int MAXC = 50005; // 小朋友数上限
const int STATES = 32; // 连续 5 个围栏的留/移状态数 2^5
const int NEG = -1000000000; // 不可达状态的初值,比任何合法得分都小
ll fence_n; // 围栏数 N
ll child_cnt; // 小朋友数 C
int head[MAXN]; // head[e]:起始围栏为 e 的小朋友链表头
int nxt_child[MAXC]; // 小朋友链表的后继
int afraid_mask[MAXC]; // 该小朋友害怕的围栏在窗口内的 5 位掩码,位 j 对应围栏 e+j
int fond_mask[MAXC]; // 该小朋友喜欢的围栏在窗口内的 5 位掩码
int edge_cnt;
int gain[MAXN][STATES]; // gain[e][s]:窗口起点为 e、5 位保留情况为 s 时高兴的小朋友数
int cur[STATES]; // 当前窗口状态的最高得分
int next_dp[STATES]; // 窗口右移一格后的最高得分
// 把小朋友挂到起点 e 的链表上。
void add_child(ll e, int afraid, int fond) {
edge_cnt++;
afraid_mask[edge_cnt] = afraid;
fond_mask[edge_cnt] = fond;
nxt_child[edge_cnt] = head[e];
head[e] = edge_cnt;
}
// 预计算 gain 表:只在小朋友视野起点处结算一次得分。
void build_gain() {
for (ll e = 1; e <= fence_n; e++) {
for (int s = 0; s < STATES; s++) {
int cnt = 0;
for (int i = head[e]; i != 0; i = nxt_child[i]) {
// 高兴:有害怕的动物被移走(掩码位为 0)或有喜欢的动物被保留(掩码位为 1)。
if ((afraid_mask[i] & ~s) != 0 || (fond_mask[i] & s) != 0) {
cnt++;
}
}
gain[e][s] = cnt;
}
}
}
void read_input() {
cin >> fence_n >> child_cnt;
for (ll k = 1; k <= child_cnt; k++) {
ll e, f, l;
cin >> e >> f >> l;
int afraid = 0;
for (ll j = 1; j <= f; j++) {
ll x;
cin >> x;
ll idx = ((x - e) % fence_n + fence_n) % fence_n; // 围栏编号换算成窗口内下标 0..4
afraid |= 1 << idx;
}
int fond = 0;
for (ll j = 1; j <= l; j++) {
ll y;
cin >> y;
ll idx = ((y - e) % fence_n + fence_n) % fence_n;
fond |= 1 << idx;
}
add_child(e, afraid, fond);
}
}
void solve() {
int best = 0;
// 圆环没有天然起点,枚举第 1 个窗口的 32 种取值,滑 N 格后要求状态回到同一个。
for (int first = 0; first < STATES; first++) {
for (int s = 0; s < STATES; s++) {
cur[s] = NEG;
}
cur[first] = gain[1][first];
// 窗口起点从 2 推到 N,每步右移一格并结算该起点的小朋友。
for (ll e = 2; e <= fence_n; e++) {
for (int s = 0; s < STATES; s++) {
next_dp[s] = NEG;
}
for (int s = 0; s < STATES; s++) {
if (cur[s] == NEG) {
continue;
}
int base = cur[s];
int t = s >> 1; // 新进视野的围栏把动物移走
if (base + gain[e][t] > next_dp[t]) {
next_dp[t] = base + gain[e][t];
}
t = (s >> 1) | 16; // 新进视野的围栏保留动物,16 = 1 << 4
if (base + gain[e][t] > next_dp[t]) {
next_dp[t] = base + gain[e][t];
}
}
for (int s = 0; s < STATES; s++) {
cur[s] = next_dp[s];
}
}
// 再做一次不带得分的平移:窗口回到起点 1(起点 N+1 模 N 后为 1),避免重复计分。
for (int s = 0; s < STATES; s++) {
next_dp[s] = NEG;
}
for (int s = 0; s < STATES; s++) {
if (cur[s] == NEG) {
continue;
}
int t = s >> 1;
if (cur[s] > next_dp[t]) {
next_dp[t] = cur[s];
}
t = (s >> 1) | 16;
if (cur[s] > next_dp[t]) {
next_dp[t] = cur[s];
}
}
// 只有首尾状态吻合的方案才对应合法的圆环安排。
if (next_dp[first] > best) {
best = next_dp[first];
}
}
cout << best << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
build_gain();
solve();
return 0;
}复杂度
- 时间复杂度:
。预计算 gain需要对每个窗口起点、每个状态扫该起点的小朋友, 而所有起点的小朋友数之和为,故为 ;DP 为 32 次枚举 步 个状态 条转移。 时约 次内层操作,最大数据实测约 0.9 s。 - 空间复杂度:
。 gain表为个整数, by_start共个二元组, DP 数组只有 32 项;峰值实测约 15 MB,远低于 128 MB。
总结
- 题面给出的关键结构是每个约束只依赖连续 5 个围栏。这直接把「
枚举」压成 「 次窗口滑动」,状态数只有 。 - 局部性的用法是双向的:向前看只用 5 位状态(转移),向后看则可以在窗口滑过时一次性结算 该起点的小朋友(得分),两者合起来才把每个小朋友不重不漏地计入一次。
- 环形问题的通用套路是枚举初窗口破环:
时窗口不会自我重叠, 枚举第 1 个窗口的 32 种取值、要求首尾状态吻合,就能在 32 条链上取最大。 - 掩码编码让「高兴判定」变成
afraid & ~s or fond & s两个按位运算,避免了在热循环里 逐条遍历小朋友的名单。

