[CSP-J2022 山东] 植树节
题目背景
受疫情影响,山东省取消了 CSP-J 2022 认证活动,并于次年三月重新命题,在省内补办比赛。
题目描述
植树节快要到了,学校要组织志愿者去给树苗浇水。
有一排树苗,编号依次是 0,1,2,…0,1,2,\dots0,1,2,…。
现有 nnn 个志愿者去给树苗浇水,第 iii 个志愿者选定了一个区间 [ai,bi]\left[a_{i},b_{i}\right][ai,bi] ,表示第 iii 个志愿者将 [ai,bi]\left[a_{i},b_{i}\right][ai,bi] 这一区间内的每一棵树都浇一次水。
如某个志愿者选择的浇水区间为 [4,9]\left[4,9\right][4,9] ,表示他将给编号为 4,5,6,7,8,94,5,6,7,8,94,5,6,7,8,9 的树各浇水一次。
当所有的志愿者完成各自所选区间的浇水后,可能有些树苗被不同的志愿者浇水多次,也可能有的树苗一次也没被浇过水。
请你求出浇水最多的树苗被浇了多少次。
输入格式
第 111 行,一个整数 nnn ,表示志愿者的人数。
第 222 行到第 n+1n+1n+1 行,每行两个整数 $a_{i},b_{i} (((i=0,1,2,\dots n-1$),表示志愿者 iii 选择的浇水区间。
输出格式
输出 111 行, 111 个整数,表示浇水最多的树苗被浇水的次数。
输入输出样例 #1
输入 #1
4
0 2
2 4
1 4
6 7
输出 #1
3
输入输出样例 #2
输入 #2
4
1000000 1000000
1000000 1000000
0 1000000
1 1000000
输出 #2
4
说明/提示
数据范围
- 对于所有的数据: n<105n<10^{5}n<105;0≤ai≤bi≤1060\le a_{i}\le b_{i}\le 10^{6}0≤ai≤bi≤106。
| 测试点编号 | ai≤a_{i}\leai≤ | bi≤b_{i}\lebi≤ | ni≤n_{i}\leni≤ | 特殊性质 |
|---|---|---|---|---|
| 1,2,31,2,31,2,3 | 10310^{3}103 | 10310^{3}103 | 10310^{3}103 | 无 |
| 4,5,6,74,5,6,74,5,6,7 | 10610^{6}106 | 10610^{6}106 | 10510^{5}105 | 无 |
| 888 | 10610^{6}106 | 10610^{6}106 | 10510^{5}105 | ai=bia_{i}=b_{i}ai=bi |
| 999 | 10610^{6}106 | 10610^{6}106 | 10510^{5}105 | ai=1,bi=103a_{i}=1,b_{i}=10^{3}ai=1,bi=103 |
| 101010 | 10610^{6}106 | 10610^{6}106 | 10510^{5}105 | 无 |
解题思路
很模板的一道差分题目,操作是区间元素全部加上一个数,在最后单点查询,竟然不需要开 long long,不知道怎么想的出这个题。
#include <bits/stdc++.h>
using namespace std;
const int maxt = 1e6 + 5;
int n, d[maxt]; // d 表示差分数组,一开始都是 0
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
int l, r;
cin >> l >> r;
d[l]++; // 差分操作
d[r + 1]--;
}
int ans = 0, ti = 0; // ti 表示当前这个地方被浇了多少次水,ans 是最大次数
for (int i = 0; i <= 1000000; i++)
{
ti += d[i];
ans = max(ans, ti);
}
cout << ans;
return 0;
}
[CSP-J2022 山东] 宴会
题目背景
受疫情影响,山东省取消了 CSP-J 2022 认证活动,并于次年三月重新命题,在省内补办比赛。
题目描述
今人不见古时月,今月曾经照古人。梦回长安,大唐风华,十里长安花,一日看尽。
唐长安城是当时世界上规模最大、建筑最宏伟、规划布局最为规范化的一座都城。其营建制度规划布局的特点是规模空前、创设皇城、三城层环、六坡利用、布局对称、街衢宽阔、坊
里齐整、形制划一、渠水纵横、绿荫蔽城、郊环祀坛。而所谓的十里长安街,位于长安城的中轴线上,即唐长安城的朱雀大街,又称承天门大街。唐朝官员们住在各个“坊”里,上朝下朝都需要通过朱雀大街。
为了保持各大家族的联系和友谊,各官员往往会每月办一次宴会。为了方便描述,我们把朱雀大街看成一个数轴,各官员所居住的“坊”缩略为数轴上的一个坐标点。大家决定选一处地点(该地点是数轴上的某一个点,不一定坐标点)办宴会。由于唐朝宵禁严格,大家又都希望交流时间尽可能长,因此想要使宴会开始时间尽可能早。又因为大唐注重礼仪,因此,参加宴会的官员会花一定时间盛装打扮过后才前往宴会地点(不一定是坐标点)。
更具体地,一条纵向的街道上(相当于一维坐标)有 nnn 个人居住,其中第 iii 个人居住在 xix_{i}xi (非负整数)位置(坐标点)上。每月他们会选择在 x0x_{0}x0(数轴上的某一个点,不一定坐标点)出举办宴会。
已知第 iii 个人从 xix_{i}xi 出发前往宴会地点 x0x_{0}x0 处需要花费 ∣xi−x0∣\left|x_{i}-x_{0}\right|∣xi−x0∣ 的时间,另外,他还需要花费 tit_{i}ti 的时间进行打扮,换言之,他共需要花费 ∣xi−x0∣+ti\left|x_{i}-x_{0}\right|+t_{i}∣xi−x0∣+ti 的时间到达宴会举办处。
假设初始时刻为 000。这 nnn 个人开始打扮和出发前往宴会处,他们想要使得宴会的开始时间尽可能早,于是向你求助,请你帮助他们确定好最优的宴会举办地点 x0x_{0}x0。
注:∣xi−x0∣\left|x_{i}-x_{0}\right|∣xi−x0∣ 表示 xix_{i}xi 与 x0x_{0}x0 之差的绝对值,且题目中 nnn 个人的居住地点坐标均为整数。
输入格式
第一行一个正整数 TTT,表示测试数据的组数。
接下来对于每组测试数据(注意:每组测试数据有 333 行数据,以下共 3×T3\times T3×T 行数据):
第一行一个正整数 nnn,表示总官员人数。
第二行共 nnn 个非负整数 x1,x2,…,xnx_{1},x_{2},\dots,x_{n}x1,x2,…,xn 分别表示这 nnn 个人在数轴上的坐标。
第三行共 nnn 个非负整数 t1,t2,…,tnt_{1},t_{2},\dots,t_{n}t1,t2,…,tn 分别表示这 nnn 个人出发前的打扮时间。
输出格式
共输出 TTT 行数据,对于每组测试数据,输出一行一个实数(如果是整数按整数输出,如果有小数,保留 111 位小数输出),表示使宴会开始时间最早的最优举办地点坐标 x0x_{0}x0。(很显然,x0x_{0}x0 都是唯一的)
输入输出样例 #1
输入 #1
7
1
0
3
2
3 1
0 0
2
1 4
0 0
3
1 2 3
0 0 0
3
1 2 3
4 1 2
3
3 3 3
5 3 3
6
5 4 7 2 10 4
3 2 5 1 4 6
输出 #1
0
2
2.5
2
1
3
6
说明/提示
样例说明
初始时刻为 000。
对于第一组测试数据只有 111 个人,坐标为 000,打扮时间为 333,很显然 x0x_{0}x0 就定在坐标 000 处,使得宴会开始时间为 333 且最早。
对于第二组测试数据有 222 个人,坐标分别为 333、111,打扮时间均为 000,很显然 x0x_{0}x0 定在坐标 222 处,使得宴会开始时间为 111 且最早。
对于第三组测试数据有 222 个人,坐标分别为 111、444,打扮时间均为 000,很显然 x0x_{0}x0 定在坐标 2.52.52.5 处,使得宴会开始时间为 1.51.51.5 且最早。
数据范围
对于 30%30\%30% 的数据,T=1,n≤100,0≤xi,ti≤1000T=1,n\le100,0\le x_{i},t_{i}\le1000T=1,n≤100,0≤xi,ti≤1000;
对于 60%60\%60% 的数据,n≤104,0≤xi,ti≤105n\le10^{4},0\le x_{i},t_{i}\le10^{5}n≤104,0≤xi,ti≤105;
对于 100%100\%100% 的数据,1≤T≤103,n≤105,0≤xi,ti≤1081\le T\le10^{3},n\le10^{5},0\le x_{i},t_{i}\le10^{8}1≤T≤103,n≤105,0≤xi,ti≤108,且保证所有测试数据的 nnn 加起来不超过 2×1052\times10^{5}2×105。
解题思路
首先把所有人按照坐标从小到大的顺序排序(相当于从左到右在大街上排列),这个就不多说了。
我们要排除一种情况,就是聚会地点坐标比所有的 “坊” 都大或者都小的情况。我们用贪心的思想去看待这个问题:如果当前聚会地点坐标比所有的 “坊” 都大或者都小(我们以都大举例),然后我把这个地点移动到坐标最大的 “坊” 的位置,那么大家的移动距离都变小了,所需的时间也就都变小了,最终答案肯定也会变小。
看看答案是怎么计算的,先计算每一个人到达聚会地点的时间,我们现在假设聚会地点的坐标为 pospospos,iii 是 “坊” 的编号,如果 xi≤posx_i \le posxi≤pos,那么这个人到达聚会地点所需要的时间就是 pos−(xi−ti)pos - (x_i - t_i)pos−(xi−ti);反之,如果 xi≥posx_i \ge posxi≥pos,所需的时间就是 (xi+ti)−pos(x_i + t_i) - pos(xi+ti)−pos。
那么我们再从整体的视角上看这个问题,假设 iii 是聚会地点左边(坐标更小)的某一个 “坊” 的坐标, jjj 是聚会地点右边(坐标更大)的某一个 “坊” 的坐标。那么左边就需要 pos−min(xi−ti)pos - min(x_i - t_i)pos−min(xi−ti) 的时间才能让所有人到达聚会地点;同理,右边就需要 max(xj+tj)−posmax(x_j + t_j) - posmax(xj+tj)−pos 的时间,那么所有人到达聚会地点的时间就是二者之间的最大值。
最后,怎么确定 pospospos 的位置呢?先维护好两个数组 L[i]L[i]L[i],R[i]R[i]R[i],L[i]L[i]L[i] 表示坐标小于等于第 iii 个 “坊” 的所有的 “坊” 的 (x−t)(x-t)(x−t) 的最小值。 R[i]R[i]R[i] 表示坐标大于第 iii 个 “坊” 的所有的 “坊” 的 (x+t)(x+t)(x+t) 的最大值。我们预先处理出来,用 for 循环从前从后扫一遍就行。然后现在我们枚举一个正整数 kkk,表示聚会地点在第 kkk 个和第 k+1k + 1k+1 个 “坊” 之间。这个位置越靠近 kkk,那么右边花费的时间就越多;相同的,越靠近 k+1k + 1k+1,左边花费的时间就更多,因为我们的目标是最大值最小,所以我们的目标是尽可能实现 pos−L[k]=R[k]−pospos - L[k] = R[k] - pospos−L[k]=R[k]−pos,这个情况下答案是最小的,否则左边花费的时间或者右边花费的时间就会变大,最终答案也会变大。那么如果上面的这个式子成立不了怎么办(求出来的 pospospos 不在 kkk 和 k+1k + 1k+1 之间)。
例如求出来 pos<xkpos < x_kpos<xk,那么说明左边花费的时间一定大于右边,最终答案是左边花费的时间,那么我们就让 pos=xkpos = x_kpos=xk 就行了;pos>xkpos > x_kpos>xk 同理。
时间复杂度就是排序的时间复杂度。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int n, L[maxn], R[maxn];
struct Person // 用结构体方便排序
{
int x, t;
} a[maxn];
void solve()
{
double min_t = 1000000000, ans;
cin >> n;
L[0] = 1000000000;
R[0] = -1000000000;
for (int i = 1; i <= n; i++)
cin >> a[i].x;
for (int i = 1; i <= n; i++)
cin >> a[i].t;
if (n == 1) // 只有一个人就直接在他家开聚会
{
cout << a[1].x << '\n';
return;
}
sort(a + 1, a + n + 1, [](const Person &x, const Person &y)
{ return x.x < y.x; }); // 排序,这里我偷懒了,大家自己写一个 cmp 就行,让坐标从小到大排序
for (int i = 1; i < n; i++) // 一个是左边一个是右边,所以一个从前往后,一个从后往前
L[i] = min(L[i - 1], a[i].x - a[i].t);
for (int i = n; i >= 2; i--)
R[i - 1] = max(R[i], a[i].x + a[i].t);
for (int i = 1; i < n; i++)
{
double b = ((double)L[i] + R[i]) / 2, pos; // b 就是求出来的 pos
if (b < a[i].x)
pos = a[i].x;
if (b > a[i + 1].x)
pos = a[i + 1].x;
if (b >= a[i].x && b <= a[i + 1].x)
pos = b;
if (max(pos - L[i], R[i] - pos) < min_t) // 如果答案更优就记录一下
min_t = max(pos - L[i], R[i] - pos), ans = pos;
}
cout << ans << '\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin >> T;
while (T--) // 多组输入
solve();
return 0;
}
[CSP-J2022 山东] 部署
题目背景
受疫情影响,山东省取消了 CSP-J 2022 认证活动,并于次年三月重新命题,在省内补办比赛。
题目描述
“万里羽书来未绝,五关烽火昼仍传。”
古时候没有现代信息化战争的技术,只能靠烽火传信和将军运筹帷幄的调兵遣将来取得战争的优势。
为了使消耗最低,现在 A 国已经在 nnn 个城市之间建好了道路和行军部署渠道,使得这 nnn 个城市都能互相到达且不存在环(即构成以 111 号城市为根节点的树型结构)。每个城市都驻扎了一定数量的兵力。
为了清晰的描述问题,我们给这 nnn 个城市进行 111 到 nnn 的编号,且 111 号城市为树的根节点(数据保证:构成以 111 号城市为根节点的一棵树)。初始时,第 iii 座城市拥有初始兵力 aia_{i}ai。
现在为测试战争部署速度,将军进行了 mmm 次测试,每次测试可能为以下两种命令的某一种:
1 x y(三个数间均用 1 个空格分开):向 xxx 号城市和以它为根的子树中的所有城市全部增兵 yyy 的数量。
2 x y(三个数间均用 1 个空格分开):向 xxx 号城市和与它直接相连(含父结点和子结点)的城市全部增兵 yyy 的数量。
mmm 条命令发布出去后,将军喊来参谋,进行了 qqq 次询问,每次询问第 xxx 座城市的最终兵力情况。
该参谋就是小虾米,他又向你求助了,请你帮助他解决问题(qqq 次询问的结果)。
输入格式
第一行一个正整数 nnn 表示城市数量。
第二行一共 nnn 个正整数 a1,a2,…ana_{1},a_{2},\dots a_{n}a1,a2,…an 表示每座城市的初始兵力。
接下来 n−1n-1n−1 行,每行两个整数 x,yx,yx,y,表示 xxx 和 yyy 城市之间有直接相连的道路。
接下来一行一个正整数 mmm,表示 mmm 次命令。
接下来 mmm 行,每行三个正整数 p,x,yp,x,yp,x,y 表示两种命令其中一种,其中 p=1p=1p=1 时表示第一种命令,p=2p=2p=2 时表示第二种命令。
接下来一行一个正整数 qqq,表示 qqq 次询问。
接下来 qqq 行,每行一个正整数 xxx ,表示询问编号为 xxx 的城市最后的兵力值。
输出格式
一共 qqq 行,每行一个正整数分别对应于每次询问的答案。
输入输出样例 #1
输入 #1
5
1 2 3 4 5
1 2
1 3
2 4
3 5
4
1 1 2
2 2 3
1 3 3
2 5 1
4
1
2
3
4
输出 #1
6
7
9
9
输入输出样例 #2
输入 #2
4
1 1 1 1
1 2
1 3
1 4
1
1 1 1
2
1
2
输出 #2
2
2
说明/提示
数据范围
对于 30%30\%30% 的数据,1≤n≤1000,1≤m≤10001\le n\le1000,1\le m\le10001≤n≤1000,1≤m≤1000;
对于 60%60\%60% 的数据,1≤n≤105,1≤m≤105,1≤q≤1051\le n\le10^{5},1\le m\le10^{5},1\le q\le10^{5}1≤n≤105,1≤m≤105,1≤q≤105;
其中 10%10\%10% 的数据树是一条链,另外 10%10\%10% 的数据只有 111 操作,另外 10%10\%10% 的数据只有 222 操作。
对于 100%100\%100% 的数据,数据保证给定的城市和道路能形成树,且 111 号城市为根节点。1≤n≤106,1≤m≤106,1≤q≤106,1≤ai≤109,1≤x≤n,1≤y≤101\le n\le10^{6},1\le m\le10^{6},1\le q\le10^{6},1\le a_{i}\le10^{9},1\le x\le n,1\le y\le101≤n≤106,1≤m≤106,1≤q≤106,1≤ai≤109,1≤x≤n,1≤y≤10。
解题思路
首先我们先要简化一下这 mmm 次操作,例如:1 1 3 1 1 2 直接变成 1 1 5 就可以了,1 的子树全部加 3,然后 1 的子树再加 2,不就等价于一次加 5 嘛,然后 2 操作同理。
接下来就是代码问题了,只要你会用代码实现上面两种操作就行了,至于时间复杂度是很宽容的,1 操作只需要一次 O(n) 的 dfsdfsdfs 就行了,至于 2 操作这里给大家稍微简单的证明一下。

对于树上这样的一条边来说,最多会存在两次操作:uuu 让 vvv 加上一个值、vvv 让 uuu 加上一个值,2 操作导致一个点让一个他相邻的点的值加上一个值的操作最多出现 2×(n−1)2 \times (n - 1)2×(n−1) 次,也是线性的。具体细节大家看下面的代码吧。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;
int n, m, q, tot, head[maxn], down[maxn], near[maxn], num[maxn];
// down[x] 表示 x 和他的子树都要加上 down[x]
// near[x] 表示 x 和他周围的点要加上的数
// num[x] 表示这个点上有多少士兵
struct edge // 这里建图用的是链式前向星,不会的自行学习
{
int nxt, to;
} e[maxn << 1];
void addedge(int u, int v)
{
e[++tot].nxt = head[u];
e[tot].to = v;
head[u] = tot;
}
void solve1(int x, int fa, int k) // 1 操作
{
k += down[x]; // 因为是 dfs,所以下面遍历的都是子树,全部需要加 k
num[x] += k;
for (int i = head[x]; i; i = e[i].nxt) // 向下遍历
{
int v = e[i].to;
if (v == fa) // 不能往上走
continue;
solve1(v, x, k);
}
return;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> num[i];
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
addedge(u, v);
addedge(v, u);
}
cin >> m;
for (int i = 1; i <= m; i++) // 先预处理一下,我们上面说的第一步
{
int op, x, y;
cin >> op >> x >> y;
if (op == 1)
down[x] += y;
if (op == 2)
near[x] += y;
}
solve1(1, 0, 0); // 1 操作
for (int i = 1; i <= n; i++) // 2 操作
{
num[i] += near[i];
for (int j = head[i]; j; j = e[j].nxt) // 枚举与 i 相邻的点
{
int v = e[j].to;
num[v] += near[i];
}
}
cin >> q;
for (int i = 1; i <= q; i++) // 问哪个输出哪个
{
int id;
cin >> id;
cout << num[id] << '\n';
}
return 0;
}
[CSP-J2022 山东] 吟诗
题目背景
受疫情影响,山东省取消了 CSP-J 2022 认证活动,并于次年三月重新命题,在省内补办比赛。
题目描述
“文章本天成,妙手偶得之。”
吟诗是表达情怀的常用手段,战争落下了帷幕,常年的军旅生活使得小虾米喜欢上了豪放派的诗歌。
这一天,小虾米突然想吟诗了。著名的豪放派诗人苏轼有“老夫聊发少年狂,左牵黄,右擎苍。”的豪放,又有“十年生死两茫茫,不思量,自难忘。”的悲怆。小虾米心向往之,于是也想用《江城子》词牌名作诗。
小虾米想作出能流传千古的诗,根据经验,如果一首诗存在妙手就能流传千古。
具体来说,一首 N 个字的诗,每个字可以用 111 到 101010 之间的某个正整数来表示。同时存在三个正整数 X,Y,Z(1≤X≤7,1≤Y≤5,1≤Z≤5)X,Y,Z\left(1\le X\le7,1\le Y\le5,1\le Z\le5\right)X,Y,Z(1≤X≤7,1≤Y≤5,1≤Z≤5),如果诗中出现了三个连续的片段使得第一个片段之和为 XXX,第二个片段之和为 YYY,第三个片段之和为 ZZZ,则小虾米认为这首诗出现了妙手。
即长度为 nnn 的序列 a1,a2,…an(1≤ai≤10)a_{1},a_{2},\dots a_{n} \left(1\le a_{i}\le10\right)a1,a2,…an(1≤ai≤10),如果存在 i,j,k,l(1≤i<j<k<l≤n)i,j,k,l\left(1\le i<j<k<l\le n\right)i,j,k,l(1≤i<j<k<l≤n) 使得ai+ai+1+…aj−1=Xa_{i}+a_{i+1}+\dots a_{j-1}=Xai+ai+1+…aj−1=X 且 aj+aj+1+…ak−1=Ya_{j}+a_{j+1}+\dots a_{k-1}=Yaj+aj+1+…ak−1=Y 且 ak+ak+1+…al−1=Za_{k}+a_{k+1}+\dots a_{l-1}=Zak+ak+1+…al−1=Z 同时成立,则认为序列出现了妙手(注:第二个片段紧接第一个片段,第三个片段紧接第二个片段)。
举例来说,如果 N=7N=7N=7,X=7X=7X=7,Y=3Y=3Y=3,Z=3Z=3Z=3,则所有长度为 777 的序列中,很显然共有 10710^{7}107 种序列,其中一种序列 [1,5,2,2,1,3,4]\left[1,5,2,2,1,3,4\right][1,5,2,2,1,3,4] 出现了妙手,因为存在三个连续的区间 [2,3]\left[2,3\right][2,3]、[4,5]\left[4,5\right][4,5]、[6,6]\left[6,6\right][6,6] 满足它们的和分别为 X=7X=7X=7,Y=3Y=3Y=3,Z=3Z=3Z=3。
小虾米想知道在给定 N,X,Y,ZN,X,Y,ZN,X,Y,Z 的前提下(共计 10n10^{n}10n 种序列,即共 10n10^{n}10n 种诗),计算有多少种存在妙手的诗,请你帮他计算出答案。
由于答案可能很大,请你将结果对 998244353998244353998244353 取模。
输入格式
一行,以空格隔开的 4 个正整数 N,X,Y,ZN,X,Y,ZN,X,Y,Z,分别表示序列长度和题目中 X,Y,ZX,Y,ZX,Y,Z 的值。
输出格式
一行,一个整数,表示答案对 998244353998244353998244353 取模的结果。
输入输出样例 #1
输入 #1
3 2 3 3
输出 #1
1
输入输出样例 #2
输入 #2
4 7 5 5
输出 #2
34
输入输出样例 #3
输入 #3
23 7 3 5
输出 #3
824896638
说明/提示
样例一说明
在所有可能的序列中,只能构造出一种序列 [2,3,3]\left[2,3,3\right][2,3,3] 满足题意,因此答案为 111。
数据范围
对于 30%30\%30% 的数据,3≤N≤53\le N\le53≤N≤5;
对于 60%60\%60% 的数据,3≤N≤203\le N\le203≤N≤20;
对于 100%100\%100% 的数据,3≤N≤40,1≤X≤7,1≤Y≤5,1≤Z≤53\le N\le40,1\le X\le7,1\le Y\le5,1\le Z\le53≤N≤40,1≤X≤7,1≤Y≤5,1≤Z≤5。
解题思路
这一把还没考 dpdpdp 呢,所以肯定是 dpdpdp(狗头)。
这一题比较有防 AK 的意思哈,观察数据范围,有可能是一个状态压缩 dpdpdp(实际上就是)。首先我们需要明确一点,计算满足条件的序列数量其实是不太好整的,因为一个序列里面可能出现好几个 “妙手”,那咋办呢?你计算不满足条件的序列数量不就行了嘛,然后拿全部的数量减一下就是答案。
然后我们设计状态 dp[i][j]dp[i][j]dp[i][j],其中 iii 表示前 iii 个数字,jjj 表示一个压缩的状态。我们来详细说一下 jjj,jjj 是一个二进制数,表示的是前 iii 个数有哪些后缀和。例如:序列 [1,2,3,4][1, 2, 3, 4][1,2,3,4],有 4、7、9、104、7、9、104、7、9、10(4,4 + 3,4 + 3 + 2,4 + 3 + 2 + 1) 一共 4 种后缀和,那么就让 j=1101001000j = 1101001000j=1101001000(二进制,最右边一位表示后缀和 1 在不在)。那么 dp[i][j]dp[i][j]dp[i][j] 就是序列的前 iii 个数,后缀和满足 jjj 有多少种可能(只是前 iii 个数字)。
那么怎么判断是不是我们想要的序列呢?假设 X=3,Y=2,Z=1X = 3,Y = 2,Z = 1X=3,Y=2,Z=1,如果在 dpdpdp 的时候,发现同时存在后缀和 1、3、61、3、61、3、6 的话,是不是就能说明存在一个 “妙手” 呢?如果遇到这种情况我们不统计就可以了。
最后我们讨论一下如何进行状态转移,显然我们应该按照 iii 从小到大的顺序进行转移,那么假设当前我们枚举到了序列的前 iii 个,再枚举这一位是几,假设是 kkk,然后第三个数,我们枚举前 i−1i - 1i−1 个数后缀和的情况,令其为 jjj。因为我们加上了一个 kkk,所以之前的后缀和都应该加上 kkk,等价于 jjj 左移 kkk 位,又因为 kkk 本身也算一个后缀和,所以再让 jjj 或上一个 (1<<(k−1))(1 << (k - 1))(1<<(k−1)) 表示 kkk 这个后缀和也存在,得到的值假设为 sss,那么就有了下面的转移方程。
s=((j<<k)∣(1<<(k−1)))s = ((j << k) | (1 << (k - 1)))s=((j<<k)∣(1<<(k−1)))
dp[i][s]=dp[i][s]+dp[i−1][j]dp[i][s] = dp[i][s] + dp[i - 1][j]dp[i][s]=dp[i][s]+dp[i−1][j]
因为 jjj 不需要储存大于 (X+Y+Z)(X+Y+Z)(X+Y+Z) 的后缀和(没意义,不能帮助我们确定是否存在 “妙手”),所以我们要保证 j≤(1<<(x+y+z))−1j \le (1 << (x + y + z)) - 1j≤(1<<(x+y+z))−1。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 45, MOD = 998244353;
int n, x, y, z, dp[maxn][1 << 18];
int main()
{
long long ans = 1;
cin >> n >> x >> y >> z;
int all = (1 << (x + y + z)) - 1, a = ((1 << (z - 1)) | (1 << (y + z - 1)) | (1 << (x + y + z - 1))); // 要保证压缩的状态小于等于 all,大家注意 a 的含义,如果一个状态包含 a,说明存在妙手
dp[0][0] = 1; // 初始状态
for (int i = 1; i <= n; i++) // 前 i 个数
{
ans = ans * 10 % MOD; // 所有的状态数,顺道求了
for (int j = 0; j <= all; j++) // 前 i - 1 个数的后缀和状态
for (int k = 1; k <= 10; k++) // 当前这个数是多少
{
int s = ((j << k) | (1 << (k - 1))) & all; // &all 是为了保证去掉溢出的内容,让状态值小于 all
if ((s | a) != s) // 说明 s 是一个不存在妙手的状态
dp[i][s] = (dp[i][s] + dp[i - 1][j]) % MOD; // 记得取模
}
}
for (int i = 0; i <= all; i++) // 去掉所有不合法的状态
ans = (ans + MOD - dp[n][i]) % MOD; // +MOD 防止 ans 被减成负数
cout << ans;
return 0;
}
本人能力有限,如有不当之处敬请指教!
1486

被折叠的 条评论
为什么被折叠?



