每局结束的桌面等于一条跳转链:T[i] 由 a[i] 与 T[最后一次出现 a[i] 的下一个位置] 拼成,统计链被经过的次数即可。
OJ: luogu
题目 ID: P17830
难度:普及+/提高-
标签:思维递推
创建: 2026-10-31 18:55
更新: 2026-10-31 20:14
形式化题目
给定长度为
一个“桌面”是一串两两不同的点数,记作
- 若
不在 中,把 接到 的右端,即 ; - 若
在 中且位置为 ,先删掉 从第 个到末尾的全部元素,再接上 ,即 。
第
要求对每个
即有多少局游戏结束时桌面上留有点数为
暴力解法
思路
最直接的做法是照题面模拟:对每个起点 table 和“点数 pos[v],一路处理到第
代码
/**
* 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-31 18:55
* update_at: 2026-10-31 19:03
*/
// brute.cpp:照题面直接模拟每一局,桌上用数组存点数顺序,pos[v] 记录 v 在桌上的位置。
// 每局 O(n),总 O(n^2),只适用于小数据。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 5;
int n, m;
int a[maxn];
int ans[maxn];
int table[maxn]; // 桌面上从左到右的点数
int pos[maxn]; // pos[v]:点数 v 在 table 中的下标,-1 表示不在桌上
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) {
for (int v = 1; v <= m; ++v) pos[v] = -1;
int top = 0; // 桌上的牌数
for (int j = i; j <= n; ++j) {
int k = a[j];
if (pos[k] != -1) { // 桌面上有点数 k 的牌,收走它和它右边的牌
int at = pos[k]; // 先记下位置,清空时 pos[k] 会被改成 -1
for (int t = at; t < top; ++t) pos[table[t]] = -1;
top = at;
}
table[top] = k;
pos[k] = top;
++top;
}
for (int t = 0; t < top; ++t) ++ans[table[t]];
}
for (int v = 1; v <= m; ++v) {
cout << ans[v] << (v == m ? '\n' : ' ');
}
return 0;
}复杂度
单局最多处理
瓶颈
瓶颈是“每一局都从头模拟一遍”。但第
要利用这个相似性,得先看清“多出来的那张牌会怎样影响后续”。关键在于:第
正解
关键观察
设第
如果
这和第
如果
并且
把这两种情况合起来。记
记
右边集合的下标严格大于
思路
把递推展开,第
链上每个位置贡献自己的点数
以样例 #1 的
第 1 局: (位置1:点数1) ──→ (位置4:点数3) ──→ 结束
第 2 局: (位置2:点数2) ──→ (位置3:点数1) ──→ (位置4:点数3) ──→ 结束
第 3 局: (位置3:点数1) ──→ (位置4:点数3) ──→ 结束
第 4 局: (位置4:点数3) ──→ 结束四局结束时的桌面分别是
于是问题变成:一条链上的每个位置,要为多少个起点服务。设
| 链经过的位置 | 结束桌面 | ||||
|---|---|---|---|---|---|
| 1 | 1 | 4 | 1、4 | 1 3 | 1 |
| 2 | 2 | 3 | 2、3、4 | 2 1 3 | 1 |
| 3 | 1 | 4 | 3、4 | 1 3 | 2 |
| 4 | 3 | 5 | 4 | 3 | 4 |
按点数汇总:点数 1 出现在位置 1 和 3,贡献 3 1 4,与样例一致。
代码
/**
* 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-31 18:55
* update_at: 2026-10-31 19:10
*/
// main.cpp:把每一局结束时留在桌上的点集写成一条「跳转链」:
// T[i] = { a[i] } + T[last[a[i]] + 1],即 a[i] 这张牌最终一定留在最左端起始终不动,
// 而它右侧的局面与「最后一次出现 a[i] 之后的那一局」完全相同。
// 于是只要统计每个位置被多少条链经过,按点数累加即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 5e5 + 5;
int n, m;
int a[maxn];
int last[300005]; // last[v]:点数 v 在序列中最后一次出现的位置
ll cnt[maxn]; // cnt[i]:有多少局游戏的链经过位置 i
ll ans[300005]; // ans[v]:桌面上留有点数 v 的局数
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
last[a[i]] = i; // 从左到右扫,最后一次赋值就是最后一次出现
}
// 每个位置自己先算一次;再把它的游戏数推给链上的下一个位置
// 下一个位置是 last[a[i]] + 1,一定比 i 大,所以从小到大扫就能一次算完
for (int i = 1; i <= n; ++i) cnt[i] = 1;
cnt[n + 1] = 0;
for (int i = 1; i <= n; ++i) {
cnt[last[a[i]] + 1] += cnt[i];
}
for (int i = 1; i <= n; ++i) {
ans[a[i]] += cnt[i];
}
for (int v = 1; v <= m; ++v) {
cout << ans[v] << (v == m ? '\n' : ' ');
}
return 0;
}复杂度
- 时间:求最后一次出现位置一次扫描,
递推一次扫描,汇总答案一次扫描,总 。 - 空间:序列、最后一次出现位置与
数组, 。
子任务覆盖分析
正解
| 测试点 | 特殊性质 | 暴力可过 | 正解可过 | ||
|---|---|---|---|---|---|
| 1~3 | 5 | 2 | 无 | 是 | 是 |
| 4~6 | 500 | 200 | 无 | 是 | 是 |
| 7~9 | 5000 | 3000 | A(单调不降) | 是 | 是 |
| 10~12 | 5000 | 3000 | 无 | 是 | 是 |
| 13,14 | 2 | 无 | 否 | 是 | |
| 15~17 | 3000 | B(块数极少) | 否 | 是 | |
| 18,19 | 3000 | 无 | 否 | 是 | |
| 20 | A(单调不降) | 否 | 是 | ||
| 21,22 | 无 | 否 | 是 | ||
| 23~25 | 无 | 否 | 是 |
正解对所有约束(含特殊性质 A、B)均采用同一套跳转链 +
总结
整道题的关键是看清“第一张牌的去向”,它决定了局面能不能被复用:
- 第一张牌在第一张之后再次出现时,它一被再次打出就带走整张桌面,于是这一局的后半段与“那张牌再次出现的位置”开局完全重合;
- 第一张牌不再出现时,它永久占住最左端,右侧的变化与“下一张牌开局”逐张对应。
两种情况都指向同一条递推
一个容易写错的地方是链上会不会重复统计同一个点数。答案是不会:递推里一旦出现同一点数的再次出现,走的就是
相关 rbook 文章
做这题用到的知识点在 rbook 算法书 上有专门章节:
| 知识点 | 对应文章 | 用在哪 |
|---|---|---|
| 缩小放大、归纳 | 《算法思维方法论》 | 只看“第 |
| 栈 | 《栈》 | 桌面的收牌动作就是“弹出 |