第十八届中南大学程序设计竞赛 赛题讲解

写在前面

此处为出题人给出的原版完整题解。

省流版题解详见题目讲解 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]\) 的答案即为:

\[\min_{j=0}^{\lceil\log_2 R\rceil} \operatorname{sum}_{R, j} - \operatorname{sum}_{L - 1, j} \]

记值域大小为 \(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\) 可供选择的餐馆的取值范围,易得方案数:

\[b_i=n-\left\lfloor\frac{k-1}{i}\right\rfloor \]

发现 \(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\) 的最短路,则人先手必胜当且仅当:

\[\operatorname{dis}(s, x) < \operatorname{dis}(t, x) \]

于是仅需以 \(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\) 时可以取得的最大战略价值,则有如下两种转移:

  1. 打下了当前城堡,准备打下一个城堡时,兵力增加了 \(b_i\)

    \[f_{i+1, j+b_i}=\max\{f_{i+1, j+b_i},\ f_{i, j}\} \]

  2. 攻打下一个城堡前,消耗兵力通过传送门守下一些城堡:

    \[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}\) 是在同一次操作被加入的,有

\[f_i = f_{i+1} + 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}\) 是否成立,若成立则

\[f_{i} = f_{i + 1} + f_{i + f_{i + 1}} + 1 \]

  • 否则可以看作 \(a_i\)\([i + 1, i + f_{i + 1}]\) 拼起来,有

\[f_{i} = f_{i + 1} + 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!

posted @ 2025-04-30 23:26  Luckyblock  阅读(157)  评论(1)    收藏  举报