第十八届中南大学程序设计竞赛 赛题讲解
写在前面
此处为出题人给出的原版完整题解。
省流版题解详见题目讲解 PPT。
预期难度分布
- 签到: A,K
- Easy: B,L
- Easy-Medium: E,F
- Medium: D,G,H
- Medium-Hard: C,M
- Hard: I,J
A 爱神的指示
给定 \(n\) 个整数 \(x_1\cdots x_n\),求和并输出
\(1\le n\le 10^3, |x_i|\le 10^3\)
1S,512MB
良心签到题,读入累加并输出即可。
连 long long
都不用开,太良心了。
#include <bits/stdc++.h>
int main() {
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
int n; std::cin >> n;
int ans2 = 0;
for (int i = 1; i <= n; ++ i) {
int x; std::cin >> x;
ans2 += x;
}
std::cout << ans2;
return 0;
}
K 延误列车
给定列车原定发车时间、到达所需站台时间、列车延误时间
判断能否赶上车
1S,128MB
签到题。
求出延误后的发车时间以及预计到达站台的时间距离 12:00 的分钟数,并比较大小即可。
格式化输入小技巧:scanf("%d:%d", &HH, &mm);
#include <bits/stdc++.h>
int main() {
int x, y, HH, mm;
scanf("%d%d", &x, &y);
scanf("%d:%d", &HH, &mm);
int t = (HH - 12) * 60 + mm + y;
printf("%s", x <= t ? "YES" : "NO");
return 0;
}
L 我即是雨
给定一 \(n\times m\) 的整数矩阵 \(a\),一个不规则形状
可以任意平移形状,保证形状全部位于矩阵内前提下,求覆盖的整数之和的最大值
\(1\le n,m\le 60, 0\le a_i\le 10^9\)
1S,256MB
模拟题。
可以通过四重循环大力枚举解决该题,以下给出 std 的的比较好写的做法。
注意到保证容器对应的所有格子在四连通下仅构成一个连通块,考虑先选择一个初始被容器覆盖的位置 \((x, y)\),然后枚举所有其他位置,若该位置为初始被容器覆盖的位置,则记录下它们的坐标与第一个选择的位置 \((x, y)\) 的差值 \((dx, dy)\)。
然后考虑枚举在平移之后,第一个选择的位置 \((x, y)\) 此时的位置 \((x', y')\),即可通过上述记录的位置的差值 \((dx, dy)\),得到此时被容器覆盖的所有位置,然后大力枚举检查是否有位置被移出了平地,若没有则统计这些位置的降雨量之和取最大值即可。
记得开 long long
。
总时间复杂度上界 \(O(n^2m^2)\) 级别。
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 65;
const pr<int, int> delta[4] = {mp(1, 0), mp(-1, 0), mp(0, 1), mp(0, -1)};
int n, m, a[kN][kN], b[kN][kN];
std::vector<pr <int, int> > pos;
bool vis[kN][kN];
void dfs(int x_, int y_, int sx_, int sy_) {
pos.push_back(mp(x_ - sx_, y_ - sy_));
vis[x_][y_] = 1;
for (auto [ex, ey]: delta) {
int nx = x_ + ex, ny = y_ + ey;
if (1 <= nx && nx <= n &&
1 <= ny && ny <= m &&
b[nx][ny] && !vis[nx][ny]) {
dfs(nx, ny, sx_, sy_);
}
}
}
int main() {
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
std::cin >> a[i][j];
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
std::cin >> b[i][j];
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
if (b[i][j]) {
dfs(i, j, i, j);
break;
}
}
if (!pos.empty()) break;
}
LL ans = 0;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
LL sum = 0;
for (auto [ex, ey]: pos) {
if (i + ex <= 0 || i + ex > n ||
j + ey <= 0 || j + ey > m) {
sum = 0;
break;
}
sum += 1ll * a[i + ex][j + ey];
}
ans = std::max(ans, sum);
}
}
std::cout << ans << "\n";
return 0;
}
B 倾盆大雨
一条线段上顺序排列 \(n\) 个坐标,第 \(i\) 个坐标海拔为 \(a_i\),若某个坐标左右两侧均有位置海拔严格大于它,则会形成水坑
给定 \(m\) 次询问,每次询问给定坐标是否形成水坑
\(1\le n\le 5\times 10^6, 1\le m\le 10^6, 1\le a_i\le 10^9\)
1S,512MB
预处理前缀、后缀最大值,可得到每个位置左侧和右侧海拔高度的最大值。若都比当前位置大,则当前位置形成水坑。
\(O(n)\) 预处理,每次询问即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define MAXN 10000005
int t,n,m,ans,x;
int a[MAXN],l[MAXN],r[MAXN];
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)l[i]=max(l[i-1],a[i]);
for(int i=n;i>=1;i--)r[i]=max(r[i+1],a[i]);
while(m--)
{
scanf("%lld",&x);
if(min(l[x-1],r[x+1])>a[x])printf("YES\n");
else printf("NO\n");
}
}
F 锦瑟花开
\(T\) 组数据,每组数据给定两个正整数 \(l,r\),从区间 \([l,r]\) 中删去尽可能少的整数,使剩余整数按位的结果不为 0,求最少的删去的整数个数
\(1\le T\le 2\times 10^5,1\le l\le r\le 2\times 10^5\)
1S,512MB
剩余的数按位与不为 0 \(\iff\) 剩余的数某二进制位上均为 1,考虑枚举保留二进制中的哪一位均为 1,然后删去范围 \([L, R]\) 中该位为 0 的数,取删数的数量的最小值即为答案。
存在多测,考虑预处理前缀和 \(\operatorname{sum}_{i, j}\) 表示 \([1, i]\) 内,二进制第 \(j\) 位上为 0 的数的数量,则一次询问 \([L, R]\) 的答案即为:
记值域大小为 \(n\),则预处理时间复杂度为 \(O(n\log n)\) 级别,单次询问时间复杂度为 \(O(\log n)\) 级别,总时间复杂度为 \(O((n + m)\log n)\) 级别。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
ll read()
{
ll x = 0; bool f = false; char c = getchar();
while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return f ? -x : x;
}
const int N = 2e5 + 5;
int sum[20][N];
void init()
{
for(int i = 0; i < 20; ++i)
{
for(int j = 1; j <= 200000; ++j)
if(!((j >> i) & 1)) sum[i][j] = sum[i][j - 1] + 1;
else sum[i][j] = sum[i][j - 1];
}
}
void solve()
{
int l = read(), r = read();
int ans = r - l + 1;
for(int i = 0; i < 20; ++i) ans = min(ans, sum[i][r] - sum[i][l - 1]);
printf("%d\n", ans);
}
signed main()
{
init();
int T = read();
while(T--) solve();
return 0;
}
E 晚上吃什么
- \(t\) 组询问,每次询问给定正整数 \(n, k\),求满足如下条件的长度为 \(n\) 的排列 \(a\) 的数量:
- \(\forall 1\le i\le n, i\times a_i \ge k\)
- \(1\le t\le 10^4, 1\le k\le n\le 2\times 10^5, \sum n\le 2\times 10^5\)
- 1S,512MB
单独考虑第 \(i\) 可供选择的餐馆的取值范围,易得方案数:
发现 \(a_i\) 可能的取值集合是 \(a_{i+1}\) 可能的取值集合的子集。
考虑按照 \(i\) 从小到大的顺序决定 \(a_i\),考虑每天还可供选择的餐馆的数量,由乘法原理可得最终答案即:
$$\prod_{i=1}^nb_i-(i-1)$$
保证了 \(\sum n\le 2\times 10^5\),于是对于每次询问都 \(O(n)\) 地计算上式的结果即可,总时间复杂度 \(O(n)\) 级别。
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
void solve() {
int n, k;
cin >> n >> k;
int ans = 1;
for (int i = 1; i <= n; i++) {
ans *= (n - (i + k - 1) / i + 1) - i + 1;
ans %= 998244353;
}
cout << ans << endl;
}
signed main() {
int t = 1;
cin >> t;
while (t--) {
solve();
}
return 0;
}
H Ave Mujica 的追逐战
给定一张 \(n\) 个点 \(m\) 条边的无向连通图,初始时人在 \(s\),鬼在 \(t\),终点在 \(x\),保证 \(s,t,x\) 互不相同,且保证有且仅有一个其他节点与终点相邻。
- 人和鬼交替移动,人先手,每次必须移动到相邻的另一节点
- 当人到达终点时,人立刻获胜
- 当在除终点之外的其他点上,人和鬼相遇时,鬼立刻获胜
- 若人无法在不被鬼在其他点抓住的前提下到达终点,鬼获胜
保证人和鬼均采取最优策略,判断是否人先手必胜。
\(3\le n,m\le 10^5, 1\le s,t,x\le n,s\neq t, s\neq x, t\neq x\)
1S,512MB
先上结论,记 \(\operatorname{dis}(u, v)\) 表示点 \(u\) 到 \(v\) 的最短路,则人先手必胜当且仅当:
于是仅需以 \(x\) 为起点跑单源最短路,或注意到边权值均为 1 仅需 BFS 即可求得单源最短路,判断上式是否成立即可。
简略证明
保证了仅有一个点与终点 \(x\) 相邻,记该点为 \(y\),则该点一定是任意到达 \(x\) 的路径的必经点。
若有 \(\operatorname{dis}(s, x) < \operatorname{dis}(t, x)\),则人沿最短路径先达终点,鬼无法中途拦截。
否则,鬼可先到达 \(y\) 并在 \(x,y\) 间反复移动。当人到达 \(y\) 时,若鬼在 \(y\) 则直接被抓,若鬼在 \(x\) 再移动一次即可抓住人,即可阻止人进入 \(x\)。
出题人题解
题意理解
本题的背景模型为鬼捉人/警捉匪模型。为方便表述,本文后续将基于“鬼”捉“人”进行解释。(我没有把Mygo成员当成鬼的意思哼啊啊啊啊啊)
在具有 \(n\) 个结点、\(m\) 条边、且无自环和重边的联通无向图中,人位于 \(s\) 号结点,鬼位于 \(t\) 结点,终点位于 \(x\) 号结点。这三个结点不重合。其中,终点只与一个其它点连接。
人和鬼轮流按照自己的最优方案行动,每次行动到达其相邻的结点,不可以不动。人先手,鬼后手。
人的目的是到达终点,人一进入终点就算胜利;而鬼的目的是阻止人到达终点,包括:抓住人(鬼抵达人当前所在的结点),或让人永远无法到达终点。
省流版
通过BFS或堆优化的Dijkstra求出人到终点的最短距离 \(dis(s,x)\),鬼到终点的最短距离 \(dis(t,x)\)。
-
当 \(dis(s,x)<dis(t,x)\) 时,鬼失败
- 此时,人能更快到达终点获胜,故鬼失败。
- 鬼是否会半路“截胡”?不会,如果鬼能半路截胡,说明其到终点的距离不应该大于人到终点的距离,与条件矛盾。
-
当 \(dis(s,x)\ge dis(t,x)\) 时,鬼胜利
- 终点 \(x\) 只与一个点相连(记为 \(y\)),则此时,鬼能与人同时或比人更快到达必经之路 \(y\)。然后鬼会在 \(x\) 与 \(y\) 之间“反复横跳”,让人要么被抓,要么永远无法抵达终点。
题目分析
由于人的目的是到达终点,所以首先考虑让人走其最短路径抵达终点。鬼为了阻止人到达终点,鬼亦应走其最短路径抵达终点。
按照该策略,当人到达终点的最短路距离(记作 \(dis(s,x)\))小于鬼到达终点的最短路距离(记作 \(dis(t,x)\)),则人胜利;否则鬼胜利。接下来,我们将分类讨论进行分析。
当 \(dis(s,x)<dis(t,x)\) 时
在该情况下,由于人距离终点更近,故总体而言,人会比鬼先到终点,从而获得胜利。
但是,人会不会在半路被鬼“截胡”呢?不会,因为如果鬼能半路截胡,说明其到终点的距离不应该大于人到终点的距离,与条件矛盾。对此,我们将使用反证法分析。
人沿着最短路径前往终点,假设在途径某点 \(p\) 时,人被鬼抓住。则此时,人与鬼从该结点 \(p\) 到达终点的距离相同,记作 \(dis(p,x)\)。
由于人一直沿着其最短路径前往终点,所以人到 \(p\) 点也是沿着最短路径的,记作 \(dis(s,p)\)。于是有:\(dis(s,x)=dis(s,p)+dis(p,x)\)。
设鬼从其起点 \(t\) 到 \(p\) 的实际走的距离为 \(Dis(t,p)\)。考虑两种情况:鬼可以沿着其到达 \(p\) 的最短路径前往该点,记作 \(dis(t,p)\),有 \(Dis(t,p)=dis(t,p)\);此外,鬼也可以绕远路前往该点,即 \(Dis(t,p)>dis(t,p)\)。综上,得 \(Dis(t,p)\ge dis(t,p)\)。
其中,\(p\) 不一定在鬼从其起点到终点的最短路径上,于是又有:\(dis(t,x)\le dis(t,p)+dis(p,x)\le Dis(t,p)+dis(p,x)\)。
由于人和鬼在 \(p\) 相遇,有:\(dis(s,p)=Dis(t,p)\)。此时联立得:\(dis(s,p)=Dis(t,p)\Rightarrow dis(s,p)+dis(p,x)=Dis(t,p)+dis(p,x)\Rightarrow dis(s,x)\ge dis(t,x)\),与条件 \(dis(s,x)<dis(t,x)\) 矛盾。
故当 \(dis(s,x)<dis(t,x)\) 时,若人以最短路径前往终点,则不存在任何一个点 \(p\),使得人和鬼相遇。又因为人能更快抵达终点,则人胜利。
当 \(dis(s,x)\ge dis(t,x)\) 时
根据题意,终点 \(x\) 只与唯一的一个结点相邻,设该点为 \(y\)。因此,\(y\) 是抵达终点的必经之路,则当 \(dis(s,x)\ge dis(t,x)\) 时,鬼能与人同时或比人提前抵达 \(y\),从而获胜。
但是我们考虑到,题目要求人和鬼每次必须行动,不可以不动,于是有以下两个问题:
- 鬼从 \(y\) 点移开后,人是否得到可乘之机,从而进入终点?
- 人是否可以选择“绕远路”,与鬼“拉扯”呢?
在某一轮移动结束后,人与鬼在 \(y\) 相遇,则人被鬼抓住,人失败。又因为 \(dis(s,x)\ge dis(t,x)\),则人不可能更早到达 \(y\)。
于是,鬼抵达 \(y\) 时,人为了不与鬼相遇,会位于 \(x,y\) 以外的结点,此时无法一步抵达终点 \(x\)。而后,鬼选择移动到 \(x\)。这时,如果人移动到 \(y\),下一步就会被鬼抓;如果人不移动到 \(y\),鬼就在下一步回到 \(y\),即重新回到了原来的情况,则人仍然无法抵达终点 \(x\)。
综上,若人与鬼同时到达 \(y\),则人会被鬼抓;若人比鬼晚到达 \(y\),则鬼可以在 \(x\) 和 \(y\) 之间反复横跳,占领了人抵达终点的必经之路,让人要么被抓,要么永远无法抵达终点。
因此,当 \(dis(s,x)\ge dis(t,x)\) 时鬼必胜。
标程
标程一
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5+5;
const LL MX = 1e18; // 设置最大值
LL n,m,s,t,x,d[N];
bool vis[N];
LL head[N],nxt[N],to[N],tot; // 链式前向星存图
struct D{
LL val,node;
bool operator < (const D &a)const{
return a.val < val;
}
};
priority_queue <D> que;
// 给图状结构加边
void add(LL u,LL v){
to[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
}
int main(){
cin>>n>>m;
cin>>s>>t>>x;
LL u,v;
for(LL i=1;i<=m;++i){
cin>>u>>v;
add(u,v);
add(v,u);
}
for(LL i=1;i<=n;++i)d[i]=MX;
d[x]=0;
// 优先队列优化的Dijkstra
que.push((D){0,x});
while(!que.empty()){
LL p=que.top().node;
que.pop();
if(vis[p])continue;
vis[p]=1;
for(LL i=head[p];i;i=nxt[i]){
if(d[to[i]]>d[p]+1){
d[to[i]]=d[p]+1;
que.push((D){d[to[i]],to[i]});
}
}
}
// 判断结果
if(d[s]<d[t])cout<<"Haruhikage\n";
else cout<<"Mygo\n";
}
标程二
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5+5;
LL n,m,s,t,x,d[N];
LL head[N],nxt[N],to[N],tot; // 链式前向星存图
queue <LL> que;
// 图状结构加边
void add(LL u,LL v){
to[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
}
int main(){
cin>>n>>m;
cin>>s>>t>>x;
LL u,v;
for(LL i=1;i<=m;++i){
cin>>u>>v;
add(u,v);
add(v,u);
}
for(LL i=1;i<=n;++i){
d[i]=-1;
}
d[x]=0;
// BFS
que.push(x);
while(!que.empty()){
LL p=que.front();
vis[p]=1;
que.pop();
for(LL i=head[p];i;i=nxt[i]){
if(d[to[i]]!=-1)continue; // 防重复
d[to[i]]=d[p]+1;
que.push(to[i]);
}
}
if(d[s]<d[t])cout<<"Haruhikage\n";
else cout<<"Mygo\n";
}
G 我要成为排版高手!!!
实现一个类 HTML 的标记语言 Kaju 的解析器,判断是否存在语法错误,并输出结果
\(1\le n\le 502, 1\le W,H\le 300\),保证 \(n\) 为偶数
2S,512MB
为了让对算法不是很熟悉的选手签完到之后也有事干的题。曾经有一道码量是这题的三倍的好题,然而因为没人验题被换下,令人感慨。
首先按顺序解析标签起始行,一些小技巧:
- 使用
substr
快速得到指定子串 - 使用
find
快速查询指定的分隔符 - 使用函数
stoi()
快速将一个数字串转换为整数 - 更简单地,使用
scanf
格式化读入:scanf("<width=%d,height=%d,left=%d,top=%d,char='%c'", &w, &h, &l, &t, &c);
标签起始行结束行的配对即括号匹配,套路用栈维护,易得每个标签对应的矩形区域及标签的优先级,并可以在按顺序解析过程中顺便判断语法错误。
解析结束后,发现数据范围不大,则可枚举所有矩形区域,按标签的优先级修改每个区域内所有位置即可求得答案。
总时间复杂度 \(O(nWH)\) 级别。
顺带一提之前的版本是每个标签起始行中所有属性的顺序任意的,为了降低难度进行了简化,然而还是没人做,令人感慨。
#include <bits/stdc++.h>
#define LL long long
const int kN = 310;
int n, W, H;
enum TagType {
Start, End
};
struct Tag {
int type;
int x, y, depth;
int width, height, left, top;
char fill;
Tag(int type_, int x_ = 1, int y_ = 1, int depth_ = 0,
int width_ = 0, int height_ = 0, int left_ = 0, int top_ = 0,
char fill_ = '0') :
type(type_), x(x_), y(y_), depth(depth_),
width(width_), height(height_), left(left_), top(top_),
fill(fill_) {}
};
std::vector<Tag> vec, st;
int dep[kN][kN];
char ans[kN][kN];
void stringSplit(std::string& s_, std::string separator,
std::vector<std::string> &res_) {
res_.clear();
std::string str = s_ + separator;
size_t t = 1, pre = 0;
while (t != str.npos) {
t = str.find(separator, pre);
if (t == str.npos) break;
res_.push_back(str.substr(pre, t - pre));
pre = t + separator.size();
}
}
Tag parse(std::string &s_) {
if (s_[0] == '/') return Tag(End);
s_ = s_.substr(1);
std::vector<std::string> attrs, temp;
stringSplit(s_, ",", attrs);
Tag now(Start);
for (auto attr: attrs) {
stringSplit(attr, "=", temp);
if (temp[0] == "width") now.width = std::stoi(temp[1]);
if (temp[0] == "height") now.height = std::stoi(temp[1]);
if (temp[0] == "left") now.left = std::stoi(temp[1]);
if (temp[0] == "top") now.top = std::stoi(temp[1]);
if (temp[0] == "char") now.fill = temp[1][1];
}
return now;
}
void solve(Tag tag_) {
auto [type, x, y, d, w, h, l, t, c] = tag_;
for (int i = x; i < x + h; ++ i) {
for (int j = y; j < y + w; ++ j) {
if (dep[i][j] <= d) {
ans[i][j] = c, dep[i][j] = d;
}
}
}
}
int main() {
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
std::cin >> n >> W >> H;
std::cin.ignore();
for (int i = 1; i <= H; ++ i) {
for (int j = 1; j <= W; ++ j) {
ans[i][j] = '0', dep[i][j] = 0;
}
}
st.push_back(Tag(Start, 1, 1, 0, W, H, 0, 0, '0'));
for (int i = 1; i <= n - 2; ++ i) {
std::string s; std::getline(std::cin, s);
Tag tag = parse(s);
if (tag.type == End) {
if (st.size() == 1) return std::cout << "NoPairedTag\n", 0;
st.pop_back();
} else {
auto [ftype, fx, fy, fd, fw, fh, fl, ft, fc] = st.back();
auto &[type, x, y, d, w, h, l, t, c] = tag;
if (w + l > fw || h + t > fh) return std::cout << "InvalidTagSize\n", 0;
x = fx + t, y = fy + l, d = fd + 1;
st.push_back(tag), solve(tag);
}
}
if (st.size() > 1) return std::cout << "NoPairedTag\n", 0;
for (int i = 1; i <= H; ++ i) {
for (int j = 1; j <= W; ++ j) {
std::cout << ans[i][j];
}
std::cout << "\n";
}
return 0;
}
D 神秘网页小游戏
初始时有 \(k\) 个士兵,需要按顺序攻下 \(n\) 个城堡,城堡 \(i\) 需要 \(a_i\) 名士兵才能攻下,攻下后获得 \(b_i\) 个士兵
刚攻下第城堡 \(i\) 时,可以派一名士兵守下城堡 \(i\) 。另有 \(m\) 个单向单人传送门 \((u_i, v_i)(u_i > v_i)\),刚攻下第城堡 \(u_i\) 时可派一名士兵去守下城堡 \(v_i\)
城堡 \(i\) 价值为 \(c_i\), 判断能否攻下所有城堡,计算最终成功攻下所有城堡的前提下,守下的城堡的价值总和的最大值
\(1\le n\le 5000, 0\le m\le 3 \times 10^5, 0\le k,a_i,b_i,c_i\le 5000\)
1S,512MB
本题的重点是观察到:某座城被守下的早晚不会影响这个城的价值,若更晚派人去守城,则在守城前会多一个人攻打城池。
也就是说,如果有城池 \(i<j<k\),且有传送门 \((k,i), (j, i)\),若最终守下了 \(i\),则在攻下 \(k\) 时再派人去守 \(i\) 更好。
传送门数量 \(m\) 可能很大,但在此发现下,所有到达 \(i\) 城堡的传送门中只有最后的那个有用,因此 \(m\) 的最大值其实是 \(n\) ,并且我们知道了每座城堡被守下的位置。
解法1:反悔贪心
考虑反悔贪心。贪心地尽可能守下当前城堡,和通过传送门可以派兵的城堡。
兵力不足时,考虑反悔之前的若干守城操作以增加兵力。每次撤回一个价值最低的守城操作,直到兵力足够攻下,或反悔所有守城操作后依然无法攻下。
用堆维护已守的城中最小的战略价值即可,总时间复杂度为 \(O(n\log n)\) 级别。
#include<iostream>
#include<vector>
#include<set>
const int N=5005;
int a[N],b[N],c[N],lst[N];
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(0),std::cout.tie(0);
int n,m,k;
std::cin>>n>>m>>k;
std::vector<std::vector<int>>to(n+5);//记录优化传送门数量后,每个点通向其他点的传送门。
for(int i=1;i<=n;i++)
std::cin>>a[i]>>b[i]>>c[i];
for(int i=1;i<=n;i++)
lst[i]=i;//进行传送门数量优化,lst[i]代表i号城堡在哪个位置被守下。
for(int i=1;i<=m;i++){
int u,v;
std::cin>>u>>v;
lst[v]=std::max(lst[v],u);//只留下最后面的传送门。
}
for(int i=1;i<=n;i++)
to[lst[i]].push_back(c[i]);
std::multiset<int>s;//维护守下城堡的战略价值的最小值。
for(int i=1;i<=n;i++){
while(k<a[i]&&s.size()){//兵力不够攻下,且还可以撤回操作时。
k++;
s.erase(s.begin());
}
if(k<a[i]){//兵力无论如何都不够。
std::cout<<-1<<'\n';
return 0;
}
k+=b[i];//攻下了该城堡,获得b[i]的兵力
for(int &x:to[i]){//尽可能守下所有城堡。
k--;
s.insert(x);
}
while(k<0){//如果守多了使兵力为负了,撤回一些操作。
k++;
s.erase(s.begin());
}
}
int ans=0;
for(auto &x:s)//累加所有守下了的城堡价值。
ans+=x;
std::cout<<ans<<'\n';
return 0;
}
解法2:动态规划
该题数据范围比较宽松,朴素 DP 也可通过。
首先为了方便实现,认为存在每个位置存在到达自身的传送门。
设 \(f_{i, j}\) 表示将要攻打 \(i\) 号城堡,当前兵力为 \(j\) 时可以取得的最大战略价值,则有如下两种转移:
-
打下了当前城堡,准备打下一个城堡时,兵力增加了 \(b_i\):
\[f_{i+1, j+b_i}=\max\{f_{i+1, j+b_i},\ f_{i, j}\} \] -
攻打下一个城堡前,消耗兵力通过传送门守下一些城堡:
\[f_{i+1, j-1}=\max\{f_{i+1, j-1},\ f_{i+1, j}+c_{v_k}\} \]
总时间复杂度 \(O\left(n\left(k+\sum b_i\right)\right)\) 级别,式子中兵力总数 \(\left(k+\sum b_i \right)\le 5000\),因此可以通过本题。
#include <iostream>
#include <vector>
#include <algorithm>
const int N = 5005;
int dp[N][N];
int a[N],b[N],c[N],lst[N];
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(0),std::cout.tie(0);
int n,m,k;
std::cin>>n>>m>>k;
std::vector<std::vector<int>>to(n+5);
for (int i=0;i<n;i++)
std::cin>>a[i]>>b[i]>>c[i];
for (int i=0;i<n;i++)
lst[i]=i;
for (int i=0;i<m;i++){
int u,v;
std::cin>>u>>v;
u--,v--;
lst[v]=std::max(lst[v],u);
}
for (int i=0;i<n;i++)
to[lst[i]].push_back(c[i]);
for (int i=0;i<n;i++)
std::sort(to[i].rbegin(),to[i].rend());
for (int i=0;i<=n;i++)
for (int j=0;j<=5000;j++)
dp[i][j]=-1e9;
dp[0][k]=0;
for (int i=0;i<n;i++){
for (int j=0;j<=5000;j++){
if (j>=a[i]&&dp[i][j]>=0)
dp[i+1][j+b[i]]=std::max(dp[i+1][j+b[i]],dp[i][j]);
}
for (int x:to[i]){
for (int j=1;j<=5000;j++)
if (dp[i+1][j]>=0)
dp[i+1][j-1]=std::max(dp[i+1][j-1],dp[i+1][j]+x);
}
}
int max=-1;
for (int i=0;i<=5000;i++)
max=std::max(max,dp[n][i]);
std::cout<<max<<'\n';
return 0;
}
C 倾盆大雨 其二
\(n\times m\) 的地图,位置 \((i, j)\) 的高度为 \(a_{i, j}\)
时刻 0 时有 \(q\) 个位置 \((x, y)\) 发生漏水,水位为 \(a_{x, y}\),将淹没相邻所有高度小于 \(a_{x, y}\) 的位置,形成水位为 \(a_{x, y}\) 的水洼,新形成的水洼会继续淹没其它相邻的位置
之后每经过 1 秒,每处水洼水位都将上升 1 单位高度,继而将周围高度小于等于水位的位置淹没
给定 \(t\),对于时间点 \(0\le i\le t-1\),求被淹没位置的数量
\(1\le n, m\le 500, 1\le q \le 50, 1\le t,a_{i, j}\le 10^5\)
4S,512MB
先考虑只有一处漏水的情况。
考虑用一组队列 \(Q_1\sim Q_{\max a}\) 记录水洼岸边,还未被淹没的位置,队列 \(Q_i\) 内存有高度为 \(i\) 的位置。
记某时刻水位高度为 \(h\),此时对队列 \(Q_h\) 进行类似于 BFS 的操作,队首的元素出列,并向周边未访问过的位置拓展:
- 若位置高度 \(a\) 小于等于 \(h\),则将其加入 \(Q_h\);
- 否则,将其加入 \(Q_a\)。
可在 \(O(nm)\) 时间复杂度内求得每个位置被淹没的时刻。然后发现数据范围较小,则对于多处漏水的情况,可以直接暴力枚举每一处漏水点,并进行上述过程,求得每个位置最早被淹没的时间 \(d_{x, y}\)。
最后对 \(d\) 统计贡献即可,总时间复杂度为 \(O(nmq)\) 级别。
#include<bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int N = 510, M = 1e5+10;
const int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
int n, m, q, t, upper, a[N][N], d[N][N], ans[M];
queue<pii> Q[M];
bool outside(int x, int y){
return x < 1 || x > n || y < 1 || y > m;
}
void solve(int x, int y){
vector<vector<bool>> vis(n+1, vector<bool>(m+1));
Q[a[x][y]].emplace(x, y);
vis[x][y] = 1;
for(int i = 0; i < t; i++){
int h = a[x][y]+i;
if(h > upper) break;
while(Q[h].size()){
auto [xx, yy] = Q[h].front();
Q[h].pop();
d[xx][yy] = min(d[xx][yy], i);
for(int k = 0; k < 4; k++){
int tx = xx+dx[k], ty = yy+dy[k];
if(outside(tx, ty) || vis[tx][ty]) continue;
if(a[tx][ty] <= h) Q[h].emplace(tx, ty);
else Q[a[tx][ty]].emplace(tx, ty);
vis[tx][ty] = 1;
}
}
}
for(int i = a[x][y]+t; i <= upper; i++)
while(Q[i].size()) Q[i].pop();
}
int main(){
ios::sync_with_stdio(0);
memset(d, 0x3f, sizeof(d));
cin >> n >> m >> q >> t;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> a[i][j], upper = max(upper, a[i][j]);
for(int i = 1; i <= q; i++){
int x, y;
cin >> x >> y;
solve(x, y);
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(d[i][j] < t) ans[d[i][j]]++;
for(int i = 0; i < t; i++)
cout << ans[i] << " ";
cout << endl;
return 0;
}
M 少女終末旅行 @ MILLENNIUM
初始时 \(n\) 个机器人排成一列,第 \(i\) 个机器人编号为 \(i\),价值为 \(a_i\),要求维护 \(m\) 次操作:
- 队列中位置为 \(x\) 的倍数的所有机器人价值加 \(y\)
- 队尾插入一个编号为 \(N+1\),价值为 \(y\) 的机器人
- 队尾的机器人出队
- 查询编号为 \(x\) 的机器人此时的价值
\(1\le n,m\le 4\times 10^5, |a_i|,|y|\le 10^6\)
保证对于每组数据,操作 4 不多于 \(10^5\) 次。
1S,512MB
知识点:枚举,暴力,模拟。
由 P1483 序列变换 改编而来,因为出题人比较良心没怎么卡常,保证卡掉暴力的同时放过了很多做法。
简化版
先考虑无入队出队操作,即只有操作 1、4 时怎么做。
操作 1 影响的位置为 \(x\) 的倍数,反过来考虑操作 1 对位置 \(p\) 有影响,当且仅当 \(x\) 是 \(p\) 的因数。
对于每次操作1,将其产生的影响 \(O(1)\) 地在位置 \(x\) 上累加;每次操作 4 枚举被查询的机器人的位置 \(p\) 的所有因数对应位置,再累加这些位置上的贡献之和即可。
考虑使用埃氏筛预处理因数,每次查询时大力枚举因数,总时间复杂度上界为 \(O(n\log n + m\sqrt n)\) 级别。
解法一
然后考虑有入队出队操作,为了方便表述,称初始的状态为时刻 0,第 \(i\) 次操作完成时为时刻 \(i\)。
保证了每个机器人只会入队及出队一次,且入队后在队列中的位置是不变的。则对于某个时刻 \(L\) 入队,时刻 \(R\) 出队的机器人的价值,在时刻 \(t\) 查询它的价值,结果即为初始价值加上时间段 \([L, \min(t, R)]\) 内,对该机器人所在位置 \(p\) 有影响的操作 1 的贡献之和。
于是考虑大力对简化版进行优化,同样考虑将每次操作 1 的影响累计在位置 \(x\) 上,考虑对每个位置 \(x\) 维护一个动态数组,每次操作 1 都将这次操作的时刻和改变量加入到数组的尾部,并维护该数组的前缀和;每次查询时同样枚举被查询的机器人的位置 \(p\) 的所有因数对应位置,在这些位置的动态数组上根据时刻二分,得到有影响的区间,再用前缀和求得有影响的操作贡献之和即可。
同样使用埃氏筛预处理因数,总时间复杂度 \(O(n\log n + m\sqrt n\log m)\) 级别,保证了操作 4 数量不是很多所以能过。
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e5 + 10;
const LL kInf = 1e18;
int n, m, pos[kN], L[kN], R[kN];
LL val[kN];
bool vis[kN];
std::vector<int> D[kN];
std::vector<pr <int, int> > vec;
std::vector<pr <int, LL> > a, modi[kN];
void init() {
for (int i = 1; i < kN; ++ i) {
for (int j = 1; i * j < kN; ++ j) {
D[i * j].push_back(i);
}
}
}
void modify(int i_, int x_, LL y_) {
if (x_ > n) return ;
if (modi[x_].empty()) modi[x_].push_back(mp(0, 0));
modi[x_].push_back(mp(i_, modi[x_].back().second + y_));
}
void push(int i_, LL y_) {
L[++ n] = i_;
val[n] = y_;
a.push_back(mp(n, y_));
pos[n] = a.size() - 1;
}
void pop(int i_) {
if (a.size() <= 1) {
std::cout << "No\n";
return ;
}
auto [id, v] = a.back();
R[id] = i_;
a.pop_back();
std::cout << "Yes\n";
}
void query(int i_, int x_) {
if (x_ > n) {
std::cout << "No\n";
return ;
}
LL sum = 0;
int p = pos[x_], L_ = L[x_], R_ = (R[x_] ? R[x_] : i_);
for (auto d: D[p]) {
if (modi[d].empty()) continue;
auto l = std::lower_bound(modi[d].begin(), modi[d].end(), mp(L_, -kInf)); -- l;
auto r = std::upper_bound(modi[d].begin(), modi[d].end(), mp(R_, kInf)); -- r;
sum += r->second - l->second;
}
std::cout << val[x_] + sum << "\n";
}
int main() {
// freopen("12.in", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
init();
std::cin >> n >> m;
a.push_back(mp(0, 0));
for (int i = 1; i <= n; ++ i) {
LL x; std::cin >> x;
a.push_back(mp(i, x));
val[i] = x;
pos[i] = i;
L[i] = 1;
}
for (int i = 1; i <= m; ++ i) {
int opt; std::cin >> opt;
if (opt == 1) {
int x; std::cin >> x;
LL y; std::cin >> y;
modify(i, x, y);
} else if (opt == 2) {
LL y; std::cin >> y;
push(i, y);
} else if (opt == 3) {
pop(i);
} else {
int x; std::cin >> x;
query(i, x);
}
}
return 0;
}
解法二
解法一太大力了,时间复杂度较高而且还不好写,有没有更优美的做法呢?
发现机器人出队后价值便确定了,于是在出队时标记机器人,并进行一次查询得到此时的价值,之后再查到该机器人时直接输出价值即可。
则仅需考虑如何查询在队列中的机器人。若某个机器人入队时刻为 \(a\),一次查询的时刻为 \(b\),则此时对该机器人有影响的操作 1 的时间范围为 \([a, b]\),这等价于直到查询时刻 \(b\) 进行的所有操作 1 的贡献,减去一段时间的前缀 \([1, a)\) 中操作 1 的贡献。仅考虑前半部分即无入队出队的简化版问题;被减去的后半部分是一个常数,即在时刻 \(a\) 对该位置的查询结果,可在入队时做一次查询得到后半部分的贡献。
实现时可以直接在机器人的初始价值上减去这部分,每次查询仅需直接查所有操作的贡献再加上初始价值,于是转换成了简化版的问题。
该解法的查询次数变多了,但是单次查询时间复杂度降低,总时间复杂度变为 \(O(n\ln n + m\sqrt{n})\) 级别。
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e5 + 10;
const LL kInf = 1e18;
int n, m, pos[kN];
LL val[kN], modi[kN];
bool died[kN];
std::vector<int> a, D[kN];
void init() {
for (int i = 1; i < kN; ++ i) {
for (int j = 1; i * j < kN; ++ j) {
D[i * j].push_back(i);
}
}
}
void modify(int x_, LL y_) {
if (x_ > n) return ;
modi[x_] += y_;
}
LL query2(int p_) {
LL sum = 0;
for (auto d: D[p_]) sum += modi[d];
return sum;
}
LL query(int x_) {
if (x_ > n) return kInf;
if (died[x_]) return val[x_];
return val[x_] + query2(pos[x_]);
}
void push(LL y_) {
a.push_back(++ n);
pos[n] = a.size();
val[n] = y_ - query2(pos[n]);
}
void pop() {
if (a.size() <= 0) {
std::cout << "No\n";
return ;
}
int id = a.back();
val[id] = query(id);
died[id] = 1;
a.pop_back();
std::cout << "Yes\n";
}
int main() {
// freopen("12.in", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
init();
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
LL x; std::cin >> x;
a.push_back(i);
val[i] = x;
pos[i] = i;
}
for (int i = 1; i <= m; ++ i) {
int opt; std::cin >> opt;
if (opt == 1) {
int x; std::cin >> x;
LL y; std::cin >> y;
modify(x, y);
} else if (opt == 2) {
LL y; std::cin >> y;
push(y);
} else if (opt == 3) {
pop();
} else {
int x; std::cin >> x;
LL ret = query(x);
if (ret == kInf) {
std::cout << "No\n";
} else {
std::cout << ret << "\n";
}
}
}
return 0;
}
解法三(std)
埃氏筛预处理因数太慢了!
考虑枚举因数的经典套路:使用欧拉筛预处理所有数的最小质因子,每次查询时不断除最小质因子将待查询位置 \(p\) 质因数分解,然后 dfs 枚举每种质因数的次数即可不重不漏地求得 \(p\) 的所有因数,再套用上述做法即可。单次查询复杂度降为 \(O(\log n +\sqrt n)\) 级别。
总时间复杂度变为 \(O(n + m(\log n + \sqrt{n}))\) 级别。
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e5 + 10;
const LL kInf = 1e18;
int n, m, minp[kN], pos[kN];
LL val[kN], modi[kN];
bool vis[kN];
bool died[kN];
std::vector<int> pri, a, D[kN];
std::vector<pr <int, int> > vec;
void init() {
for (int i = 2; i < kN; ++ i) {
if (!vis[i]) pri.push_back(i), minp[i] = i;
for (auto p: pri) {
if (i * p >= kN) break;
vis[i * p] = 1;
minp[i * p] = p;
if (i % p == 0) break;
}
}
}
void modify(int x_, LL y_) {
if (x_ > n) return ;
modi[x_] += y_;
}
LL dfs(int now_, int d_) {
if (now_ >= (int) vec.size()) return modi[d_];
LL ret = 0;
int p = vec[now_].first;
for (int i = 0; i <= vec[now_].second; ++ i) {
ret += dfs(now_ + 1, d_);
d_ *= p;
}
return ret;
}
LL query2(int p_) {
int temp = p_;
vec.clear();
while (temp != 1) {
int p = minp[temp];
vec.push_back(mp(p, 0));
while (temp % p == 0) ++ vec.back().second, temp /= p;
}
return dfs(0, 1);
}
LL query(int x_) {
if (x_ > n) return kInf;
if (died[x_]) return val[x_];
return val[x_] + query2(pos[x_]);
}
void push(LL y_) {
a.push_back(++ n);
pos[n] = a.size();
val[n] = y_ - query2(pos[n]);
}
void pop() {
if (a.size() <= 0) {
std::cout << "No\n";
return ;
}
int id = a.back();
val[id] = query(id);
died[id] = 1;
a.pop_back();
std::cout << "Yes\n";
}
int main() {
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
init();
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
LL x; std::cin >> x;
a.push_back(i);
val[i] = x;
pos[i] = i;
}
for (int i = 1; i <= m; ++ i) {
int opt; std::cin >> opt;
if (opt == 1) {
int x; std::cin >> x;
LL y; std::cin >> y;
modify(x, y);
} else if (opt == 2) {
LL y; std::cin >> y;
push(y);
} else if (opt == 3) {
pop();
} else {
int x; std::cin >> x;
LL ret = query(x);
if (ret == kInf) {
std::cout << "No\n";
} else {
std::cout << ret << "\n";
}
}
}
return 0;
}
解法四
因为时限比较宽松,造数据也没有特别地把乱搞卡常卡掉,一些实现比较简单乱搞做法也能过,甚至跑得很快甚至跑得很快。
比如一位验题人给出了一个根号分治的做法。
大家充分发挥人类智慧吼吼吼吼
I 连续插入
定义一个正整数序列是合法的,当且仅当可以通过在空序列上重复以下操作来获得它:
- 选择一个正整数 \(k\),然后将 \((1, 2, ..., k-1, k)\) 插入序列的某个位置。
给定一个长度为 \(n\) 的正整数序列 \(a_1\cdots a_n\),请求出其中有多少不同的子串,且这些子串是合法的。
\(1\le a_i\le n\le 5\times 10^5\)
1S,512MB
合法子串长什么样子呢?
- 一段合法子串的开头一定是 1
- 如果一个子串 \([l,r]\) 合法,那么 \([l,r−1]\) 也合法
- 若 \(a_i +1=a_{i+1}\),则他们是在同一次操作被加入的
- 若子串 \([l,r]\) 中存在极大的子串 \([p,q]\) 合法,则判断 \([l,r]\) 合法等价于把 \([p,q]\) 删除后是否合法
上述四个结论正确性显然,证明略
动态规划做法
根据上述结论可得到一个动态规划解法,考虑拓展合法子串的定义,认为合法子串的后缀也是合法的。
记 \(f_i\) 表示以位置 \(i\) 开头的最长合法子串长度(不保证 \(a_i=1\)),倒序枚举位置进行 DP,转移分三种情况:
- \(a_i + 1 = a_{i+1}\):则 \(a_i,a_{i+1}\) 是在同一次操作被加入的,有
- \(a_i + 1\not= a_{i + 1},\ a_{i+1} = 1\):考虑删除 \([i+1, i+f_{i+1}]\) 后是否合法,即检查 \(a_{i} + 1 = a_{i + f_{i + 1} + 1}\) 是否成立,若成立则
- 否则可以看作 \(a_i\) 和 \([i + 1, i + f_{i + 1}]\) 拼起来,有
- 若上述均不满足,则无法拼接,有 \(f_i = 1\)
将 \(a_i=1\) 的位置累加 \(f_i\) 即答案,总时间复杂度 \(O(n)\) 级别。
// Author: liqing
// Created: Sat May 7 21:06:30 2022
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e5+10;
int a[N], f[N];
int main()
{
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
cin.tie(nullptr)->sync_with_stdio(false);
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
f[n - 1] = n - 1;
LL ans = 0;
for (int i = n - 1; i >= 0; i--) {
if(i < n - 1) {
if (a[i] == a[i + 1] - 1) f[i] = f[i + 1];
else {
if(a[i + 1] == 1) {
if(f[i + 1] != n - 1 && a[i] == a[f[i + 1] + 1] - 1) f[i] = f[f[i + 1] + 1];
else f[i] = f[i + 1];
}
else f[i] = i;
}
}
if (a[i] == 1) ans += f[i] - i + 1;
}
cout << ans << "\n";
return 0;
}
贪心做法
考虑按顺序枚举 \(a_1\sim a_n\),计算有多少个以 \(a_i\) 结尾的子串 \([j,i]\) 合法。记贡献为 \(g_i\),则 \(\sum g_i\) 即答案。
当 \(a_i=1\) 时,显然有 \(g_i = g_{i - 1} + 1\)。
当 \(a_i\not=1\) 时,则应当找到之前最近的一个位置 \(j\) 满足:
- \(a_j + 1= a_i\);
- \([j + 1, i - 1]\) 是合法子串。
此时当且仅当将 \(a_i\) 拼接上去后,才可得到所有以 \(a_i\) 结尾的合法子串,则有 \(g_{i} = g_{j}\)。
发现上述过程中,需要对所有 \(a_i\) 尝试向左匹配一个最近的 \(a_j = a_{i} - 1\),实际上类似括号匹配。考虑用栈进行匹配,记栈大小为 \(\operatorname{size}\),栈顶元素为 \(\operatorname{top}\)。
-
当 \(a_i=1\) 时,向栈内加入元素 \(1\);
-
当 \(a_i\neq 1\) 时,不断执行以下操作:
- 若栈为空,停止循环;
- 若 \(\operatorname{top}=a_i-1\),说明找到了 \(a_i\) 匹配的位置 \(j\),则弹出栈顶元素,令 \(a_i\) 入栈,并停止循环;
- 否则,弹出栈顶元素。
-
实际上是将每次操作插入的一段数 \(1\sim k\) 在栈中缩成了当前最末尾的数,弹栈相当于不断删去与 \(a_i\) 匹配的位置 \(j\) 之后的合法的子串 \([j + 1, i - 1]\)。
-
然后可以发现:此时 \(\operatorname{size}\) 即为 \(g_j = g_i\)。
每个位置仅会入栈出栈一次,则总时间复杂度 \(O(n)\) 级别。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
ll read()
{
ll x = 0; bool f = false; char c = getchar();
while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return f ? -x : x;
}
const int N = 5e5 + 5;
int n;
int sta[N], top;
void solve()
{
n = read();
ll ans = 0;
for(int i = 1; i <= n; ++i)
{
int a = read();
if(a == 1) sta[++top] = 1;
else if(top && sta[top] == a - 1) --top, sta[++top] = a;
else
{
while(top && sta[top] != a - 1) --top;
if(top) --top, sta[++top] = a;
}
ans += top;
}
printf("%lld\n", ans);
}
int main()
{
int T = 1;
while(T--) solve();
return 0;
}
J 金丹
定义对一个字符串 \(S\) 的划分为:
- 将 \(S\) 分割为若干非空连续子串 \(s_1,s_2,\ldots,s_k\)(\(k \geq 1\)),使得 \(s_1 + s_2 + \cdots + s_k = S\)
现给定一个长度为 \(n\) 的仅包含小写英文字母的字符串 \(S\),对于其所有可能的划分方案:
- 初始化一个空的字典树,初始时仅含根节点;
- 将所有子串按序插入该字典树;
- 记录最终树中总节点数(包含根节点)
计算所有划分方案对应字典树节点数之和,对 \(10^9+7\) 取模。
\(1\le n\le 2000\)
2S,512MB
出题人题解
依次统计 2^{n-1}
个字典树难以实现,考虑统计每个节点在多少棵字典树中出现。
首先建出包含原串所有子串的树,具体而言是将 [1,n],[2,n],[3,n],...,[n,n]
共 n
个字符串插入字典树。
考虑如何计算一个节点的答案:建树过程中可以处理出经过该节点的 m
个子串的集合,并且根据插入编号 [i,n]
得到 m
个串的起始位置,这 m
个串显然是相同的。子串之间可能有重叠,考虑正难则反。
问题转化为:已知原串中有 m
个相同的长度为 k
的子串,求有多少划分数使得这些子串都不出现。
长度为 n
的字符串有 n-1
个间隔,所有可能的子串划分情况是 2^{n-1}
。接下来以|表示该间隔划分,-表示该位置不划分。
一个划分形如--|---|--|
先考虑简单的情况,m=1
时一个子串要出现的条件是这个串的划分情况形如 |----|,即两端必须划分,中间不能划分,这会固定k+1
个间隔。答案为 ans*(1-\frac{1}{2^{k+1}})
。如果 m
个子串不重叠,即每次执行 ans*(1-\frac{1}{2^{k+1}})
。
假设有子串重叠,比如 1---2------1---2,显然串2出现时串1必定不出现。即多个串重叠时,有且仅有一个串能出现。
考虑顺次统计答案,记 f[i] 表示截至到串 i,串 1-i 都不出现的方案数。
有 f[i] = f[i-1] - f[j]/2^k (j 是与i没有重叠关系的最右串)
f[j]/2^k 就是在 f[i-1] 中不包含 i-1 但包含 i 的方案数,若j=i-1则显然,否则根据定义,因为没有重叠关系,所以 f[j] 的方案中,串i对应的k个位置一定是任选的,f[j]/2^k就是 f[j] 中包含i的方案数,又因为包含i必不包含i-1,所以就是 f[i-1] 中i出现的方案数。
#include<bits/stdc++.h>
using namespace std;
const int N=2005,mod=1e9+7,Inv2=((mod+1)>>1);
char s[N];vector<int>st[N*N];int n,cnt=1,ans=0;
int base2[N],inv2[N],trie[N*N][27],f[N];
void insert(int l,int r) {
for(int i=l,p=1;i<=r;i++) {
int ch=s[i]-'a';
if(!trie[p][ch])trie[p][ch]=++cnt;
p=trie[p][ch];st[p].push_back(l);
}
return;
}
int work(int u,int dep) {
int m=st[u].size();//有m个子串经过了这个点
f[0]=base2[n-1];//2^(n-1) 种划分方式
for(int i=1;i<=m;i++)//依次枚举m段关键串,保证只转移不合法的情况
f[i]=((f[i-1]-1ll*f[lower_bound(st[u].begin(),st[u].end(),st[u][i-1]-dep+1)
-st[u].begin()]*inv2[dep-(st[u][i-1]==1)]%mod)%mod+mod)%mod;
return(base2[n-1]-f[m]+mod)%mod;
//f[m]是不包含这个节点的划分方式
}
void dfs(int u,int dep) {
if(dep)ans=(ans+work(u,dep))%mod;
for(int x=0;x<26;x++)
if(trie[u][x])dfs(trie[u][x],dep+1);
return;
}
int main() {
scanf("%d %s",&n,s+1);base2[0]=inv2[0]=1;
for(int i=1;i<=n;i++)insert(i,n);
for(int i=1;i<=n;i++)base2[i]=(base2[i-1]<<1)%mod;
for(int i=1;i<=n;i++)inv2[i]=(1ll*inv2[i-1]*Inv2)%mod;
dfs(1,0);
printf("%d\n",(ans+base2[n-1])%mod);
return 0;
}
写在最后
Thanks for listening!