[CSP-J2022 山东] 植树节
题目背景
受疫情影响,山东省取消了 CSP-J 2022 认证活动,并于次年三月重新命题,在省内补办比赛。
题目描述
植树节快要到了,学校要组织志愿者去给树苗浇水。
有一排树苗,编号依次是 0 , 1 , 2 , … 0,1,2,\dots 0,1,2,…。
现有 n n n 个志愿者去给树苗浇水,第 i i i 个志愿者选定了一个区间 [ a i , b i ] \left[a_{i},b_{i}\right] [ai,bi] ,表示第 i i i 个志愿者将 [ a i , b i ] \left[a_{i},b_{i}\right] [ai,bi] 这一区间内的每一棵树都浇一次水。
如某个志愿者选择的浇水区间为 [ 4 , 9 ] \left[4,9\right] [4,9] ,表示他将给编号为 4 , 5 , 6 , 7 , 8 , 9 4,5,6,7,8,9 4,5,6,7,8,9 的树各浇水一次。
当所有的志愿者完成各自所选区间的浇水后,可能有些树苗被不同的志愿者浇水多次,也可能有的树苗一次也没被浇过水。
请你求出浇水最多的树苗被浇了多少次。
输入格式
第 1 1 1 行,一个整数 n n n ,表示志愿者的人数。
第 2 2 2 行到第 n + 1 n+1 n+1 行,每行两个整数 $a_{i},b_{i} ( ( (i=0,1,2,\dots n-1$),表示志愿者 i i i 选择的浇水区间。
输出格式
输出 1 1 1 行, 1 1 1 个整数,表示浇水最多的树苗被浇水的次数。
输入输出样例 #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 < 1 0 5 n<10^{5} n<105; 0 ≤ a i ≤ b i ≤ 1 0 6 0\le a_{i}\le b_{i}\le 10^{6} 0≤ai≤bi≤106。
测试点编号 | a i ≤ a_{i}\le ai≤ | b i ≤ b_{i}\le bi≤ | n i ≤ n_{i}\le ni≤ | 特殊性质 |
---|---|---|---|---|
1 , 2 , 3 1,2,3 1,2,3 | 1 0 3 10^{3} 103 | 1 0 3 10^{3} 103 | 1 0 3 10^{3} 103 | 无 |
4 , 5 , 6 , 7 4,5,6,7 4,5,6,7 | 1 0 6 10^{6} 106 | 1 0 6 10^{6} 106 | 1 0 5 10^{5} 105 | 无 |
8 8 8 | 1 0 6 10^{6} 106 | 1 0 6 10^{6} 106 | 1 0 5 10^{5} 105 | a i = b i a_{i}=b_{i} ai=bi |
9 9 9 | 1 0 6 10^{6} 106 | 1 0 6 10^{6} 106 | 1 0 5 10^{5} 105 | a i = 1 , b i = 1 0 3 a_{i}=1,b_{i}=10^{3} ai=1,bi=103 |
10 10 10 | 1 0 6 10^{6} 106 | 1 0 6 10^{6} 106 | 1 0 5 10^{5} 105 | 无 |
解题思路
很模板的一道差分题目,操作是区间元素全部加上一个数,在最后单点查询,竟然不需要开 long long,不知道怎么想的出这个题。
#include <bits/stdc++.h>using namespace std;const int maxt = 1e6 + 5;
int n, d[maxt]; // d 表示差分数组,一开始都是 0int 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 认证活动,并于次年三月重新命题,在省内补办比赛。
题目描述
今人不见古时月,今月曾经照古人。梦回长安,大唐风华,十里长安花,一日看尽。
唐长安城是当时世界上规模最大、建筑最宏伟、规划布局最为规范化的一座都城。其营建制度规划布局的特点是规模空前、创设皇城、三城层环、六坡利用、布局对称、街衢宽阔、坊
里齐整、形制划一、渠水纵横、绿荫蔽城、郊环祀坛。而所谓的十里长安街,位于长安城的中轴线上,即唐长安城的朱雀大街,又称承天门大街。唐朝官员们住在各个“坊”里,上朝下朝都需要通过朱雀大街。
为了保持各大家族的联系和友谊,各官员往往会每月办一次宴会。为了方便描述,我们把朱雀大街看成一个数轴,各官员所居住的“坊”缩略为数轴上的一个坐标点。大家决定选一处地点(该地点是数轴上的某一个点,不一定坐标点)办宴会。由于唐朝宵禁严格,大家又都希望交流时间尽可能长,因此想要使宴会开始时间尽可能早。又因为大唐注重礼仪,因此,参加宴会的官员会花一定时间盛装打扮过后才前往宴会地点(不一定是坐标点)。
更具体地,一条纵向的街道上(相当于一维坐标)有 n n n 个人居住,其中第 i i i 个人居住在 x i x_{i} xi (非负整数)位置(坐标点)上。每月他们会选择在 x 0 x_{0} x0(数轴上的某一个点,不一定坐标点)出举办宴会。
已知第 i i i 个人从 x i x_{i} xi 出发前往宴会地点 x 0 x_{0} x0 处需要花费 ∣ x i − x 0 ∣ \left|x_{i}-x_{0}\right| ∣xi−x0∣ 的时间,另外,他还需要花费 t i t_{i} ti 的时间进行打扮,换言之,他共需要花费 ∣ x i − x 0 ∣ + t i \left|x_{i}-x_{0}\right|+t_{i} ∣xi−x0∣+ti 的时间到达宴会举办处。
假设初始时刻为 0 0 0。这 n n n 个人开始打扮和出发前往宴会处,他们想要使得宴会的开始时间尽可能早,于是向你求助,请你帮助他们确定好最优的宴会举办地点 x 0 x_{0} x0。
注: ∣ x i − x 0 ∣ \left|x_{i}-x_{0}\right| ∣xi−x0∣ 表示 x i x_{i} xi 与 x 0 x_{0} x0 之差的绝对值,且题目中 n n n 个人的居住地点坐标均为整数。
输入格式
第一行一个正整数 T T T,表示测试数据的组数。
接下来对于每组测试数据(注意:每组测试数据有 3 3 3 行数据,以下共 3 × T 3\times T 3×T 行数据):
第一行一个正整数 n n n,表示总官员人数。
第二行共 n n n 个非负整数 x 1 , x 2 , … , x n x_{1},x_{2},\dots,x_{n} x1,x2,…,xn 分别表示这 n n n 个人在数轴上的坐标。
第三行共 n n n 个非负整数 t 1 , t 2 , … , t n t_{1},t_{2},\dots,t_{n} t1,t2,…,tn 分别表示这 n n n 个人出发前的打扮时间。
输出格式
共输出 T T T 行数据,对于每组测试数据,输出一行一个实数(如果是整数按整数输出,如果有小数,保留 1 1 1 位小数输出),表示使宴会开始时间最早的最优举办地点坐标 x 0 x_{0} x0。(很显然, x 0 x_{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
说明/提示
样例说明
初始时刻为 0 0 0。
对于第一组测试数据只有 1 1 1 个人,坐标为 0 0 0,打扮时间为 3 3 3,很显然 x 0 x_{0} x0 就定在坐标 0 0 0 处,使得宴会开始时间为 3 3 3 且最早。
对于第二组测试数据有 2 2 2 个人,坐标分别为 3 3 3、 1 1 1,打扮时间均为 0 0 0,很显然 x 0 x_{0} x0 定在坐标 2 2 2 处,使得宴会开始时间为 1 1 1 且最早。
对于第三组测试数据有 2 2 2 个人,坐标分别为 1 1 1、 4 4 4,打扮时间均为 0 0 0,很显然 x 0 x_{0} x0 定在坐标 2.5 2.5 2.5 处,使得宴会开始时间为 1.5 1.5 1.5 且最早。
数据范围
对于 30 % 30\% 30% 的数据, T = 1 , n ≤ 100 , 0 ≤ x i , t i ≤ 1000 T=1,n\le100,0\le x_{i},t_{i}\le1000 T=1,n≤100,0≤xi,ti≤1000;
对于 60 % 60\% 60% 的数据, n ≤ 1 0 4 , 0 ≤ x i , t i ≤ 1 0 5 n\le10^{4},0\le x_{i},t_{i}\le10^{5} n≤104,0≤xi,ti≤105;
对于 100 % 100\% 100% 的数据, 1 ≤ T ≤ 1 0 3 , n ≤ 1 0 5 , 0 ≤ x i , t i ≤ 1 0 8 1\le T\le10^{3},n\le10^{5},0\le x_{i},t_{i}\le10^{8} 1≤T≤103,n≤105,0≤xi,ti≤108,且保证所有测试数据的 n n n 加起来不超过 2 × 1 0 5 2\times10^{5} 2×105。
解题思路
首先把所有人按照坐标从小到大的顺序排序(相当于从左到右在大街上排列),这个就不多说了。
我们要排除一种情况,就是聚会地点坐标比所有的 “坊” 都大或者都小的情况。我们用贪心的思想去看待这个问题:如果当前聚会地点坐标比所有的 “坊” 都大或者都小(我们以都大举例),然后我把这个地点移动到坐标最大的 “坊” 的位置,那么大家的移动距离都变小了,所需的时间也就都变小了,最终答案肯定也会变小。
看看答案是怎么计算的,先计算每一个人到达聚会地点的时间,我们现在假设聚会地点的坐标为 p o s pos pos, i i i 是 “坊” 的编号,如果 x i ≤ p o s x_i \le pos xi≤pos,那么这个人到达聚会地点所需要的时间就是 p o s − ( x i − t i ) pos - (x_i - t_i) pos−(xi−ti);反之,如果 x i ≥ p o s x_i \ge pos xi≥pos,所需的时间就是 ( x i + t i ) − p o s (x_i + t_i) - pos (xi+ti)−pos。
那么我们再从整体的视角上看这个问题,假设 i i i 是聚会地点左边(坐标更小)的某一个 “坊” 的坐标, j j j 是聚会地点右边(坐标更大)的某一个 “坊” 的坐标。那么左边就需要 p o s − m i n ( x i − t i ) pos - min(x_i - t_i) pos−min(xi−ti) 的时间才能让所有人到达聚会地点;同理,右边就需要 m a x ( x j + t j ) − p o s max(x_j + t_j) - pos max(xj+tj)−pos 的时间,那么所有人到达聚会地点的时间就是二者之间的最大值。
最后,怎么确定 p o s pos pos 的位置呢?先维护好两个数组 L [ i ] L[i] L[i], R [ i ] R[i] R[i], L [ i ] L[i] L[i] 表示坐标小于等于第 i i i 个 “坊” 的所有的 “坊” 的 ( x − t ) (x-t) (x−t) 的最小值。 R [ i ] R[i] R[i] 表示坐标大于第 i i i 个 “坊” 的所有的 “坊” 的 ( x + t ) (x+t) (x+t) 的最大值。我们预先处理出来,用 for 循环从前从后扫一遍就行。然后现在我们枚举一个正整数 k k k,表示聚会地点在第 k k k 个和第 k + 1 k + 1 k+1 个 “坊” 之间。这个位置越靠近 k k k,那么右边花费的时间就越多;相同的,越靠近 k + 1 k + 1 k+1,左边花费的时间就更多,因为我们的目标是最大值最小,所以我们的目标是尽可能实现 p o s − L [ k ] = R [ k ] − p o s pos - L[k] = R[k] - pos pos−L[k]=R[k]−pos,这个情况下答案是最小的,否则左边花费的时间或者右边花费的时间就会变大,最终答案也会变大。那么如果上面的这个式子成立不了怎么办(求出来的 p o s pos pos 不在 k k k 和 k + 1 k + 1 k+1 之间)。
例如求出来 p o s < x k pos < x_k pos<xk,那么说明左边花费的时间一定大于右边,最终答案是左边花费的时间,那么我们就让 p o s = x k pos = x_k pos=xk 就行了; p o s > x k pos > x_k pos>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 就是求出来的 posif (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 国已经在 n n n 个城市之间建好了道路和行军部署渠道,使得这 n n n 个城市都能互相到达且不存在环(即构成以 1 1 1 号城市为根节点的树型结构)。每个城市都驻扎了一定数量的兵力。
为了清晰的描述问题,我们给这 n n n 个城市进行 1 1 1 到 n n n 的编号,且 1 1 1 号城市为树的根节点(数据保证:构成以 1 1 1 号城市为根节点的一棵树)。初始时,第 i i i 座城市拥有初始兵力 a i a_{i} ai。
现在为测试战争部署速度,将军进行了 m m m 次测试,每次测试可能为以下两种命令的某一种:
1 x y
(三个数间均用 1 个空格分开):向 x x x 号城市和以它为根的子树中的所有城市全部增兵 y y y 的数量。
2 x y
(三个数间均用 1 个空格分开):向 x x x 号城市和与它直接相连(含父结点和子结点)的城市全部增兵 y y y 的数量。
m m m 条命令发布出去后,将军喊来参谋,进行了 q q q 次询问,每次询问第 x x x 座城市的最终兵力情况。
该参谋就是小虾米,他又向你求助了,请你帮助他解决问题( q q q 次询问的结果)。
输入格式
第一行一个正整数 n n n 表示城市数量。
第二行一共 n n n 个正整数 a 1 , a 2 , … a n a_{1},a_{2},\dots a_{n} a1,a2,…an 表示每座城市的初始兵力。
接下来 n − 1 n-1 n−1 行,每行两个整数 x , y x,y x,y,表示 x x x 和 y y y 城市之间有直接相连的道路。
接下来一行一个正整数 m m m,表示 m m m 次命令。
接下来 m m m 行,每行三个正整数 p , x , y p,x,y p,x,y 表示两种命令其中一种,其中 p = 1 p=1 p=1 时表示第一种命令, p = 2 p=2 p=2 时表示第二种命令。
接下来一行一个正整数 q q q,表示 q q q 次询问。
接下来 q q q 行,每行一个正整数 x x x ,表示询问编号为 x x x 的城市最后的兵力值。
输出格式
一共 q q q 行,每行一个正整数分别对应于每次询问的答案。
输入输出样例 #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 ≤ 1000 1\le n\le1000,1\le m\le1000 1≤n≤1000,1≤m≤1000;
对于 60 % 60\% 60% 的数据, 1 ≤ n ≤ 1 0 5 , 1 ≤ m ≤ 1 0 5 , 1 ≤ q ≤ 1 0 5 1\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% 的数据只有 1 1 1 操作,另外 10 % 10\% 10% 的数据只有 2 2 2 操作。
对于 100 % 100\% 100% 的数据,数据保证给定的城市和道路能形成树,且 1 1 1 号城市为根节点。 1 ≤ n ≤ 1 0 6 , 1 ≤ m ≤ 1 0 6 , 1 ≤ q ≤ 1 0 6 , 1 ≤ a i ≤ 1 0 9 , 1 ≤ x ≤ n , 1 ≤ y ≤ 10 1\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\le10 1≤n≤106,1≤m≤106,1≤q≤106,1≤ai≤109,1≤x≤n,1≤y≤10。
解题思路
首先我们先要简化一下这 m m m 次操作,例如:1 1 3
1 1 2
直接变成 1 1 5
就可以了,1 的子树全部加 3,然后 1 的子树再加 2,不就等价于一次加 5 嘛,然后 2 操作同理。
接下来就是代码问题了,只要你会用代码实现上面两种操作就行了,至于时间复杂度是很宽容的,1 操作只需要一次 O(n) 的 d f s dfs dfs 就行了,至于 2 操作这里给大家稍微简单的证明一下。
对于树上这样的一条边来说,最多会存在两次操作: u u u 让 v v v 加上一个值、 v v v 让 u u u 加上一个值,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,所以下面遍历的都是子树,全部需要加 knum[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 个字的诗,每个字可以用 1 1 1 到 10 10 10 之间的某个正整数来表示。同时存在三个正整数 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),如果诗中出现了三个连续的片段使得第一个片段之和为 X X X,第二个片段之和为 Y Y Y,第三个片段之和为 Z Z Z,则小虾米认为这首诗出现了妙手。
即长度为 n n n 的序列 a 1 , a 2 , … a n ( 1 ≤ a i ≤ 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) 使得 a i + a i + 1 + … a j − 1 = X a_{i}+a_{i+1}+\dots a_{j-1}=X ai+ai+1+…aj−1=X 且 a j + a j + 1 + … a k − 1 = Y a_{j}+a_{j+1}+\dots a_{k-1}=Y aj+aj+1+…ak−1=Y 且 a k + a k + 1 + … a l − 1 = Z a_{k}+a_{k+1}+\dots a_{l-1}=Z ak+ak+1+…al−1=Z 同时成立,则认为序列出现了妙手(注:第二个片段紧接第一个片段,第三个片段紧接第二个片段)。
举例来说,如果 N = 7 N=7 N=7, X = 7 X=7 X=7, Y = 3 Y=3 Y=3, Z = 3 Z=3 Z=3,则所有长度为 7 7 7 的序列中,很显然共有 1 0 7 10^{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 = 7 X=7 X=7, Y = 3 Y=3 Y=3, Z = 3 Z=3 Z=3。
小虾米想知道在给定 N , X , Y , Z N,X,Y,Z N,X,Y,Z 的前提下(共计 1 0 n 10^{n} 10n 种序列,即共 1 0 n 10^{n} 10n 种诗),计算有多少种存在妙手的诗,请你帮他计算出答案。
由于答案可能很大,请你将结果对 998244353 998244353 998244353 取模。
输入格式
一行,以空格隔开的 4 个正整数 N , X , Y , Z N,X,Y,Z N,X,Y,Z,分别表示序列长度和题目中 X , Y , Z X,Y,Z X,Y,Z 的值。
输出格式
一行,一个整数,表示答案对 998244353 998244353 998244353 取模的结果。
输入输出样例 #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] 满足题意,因此答案为 1 1 1。
数据范围
对于 30 % 30\% 30% 的数据, 3 ≤ N ≤ 5 3\le N\le5 3≤N≤5;
对于 60 % 60\% 60% 的数据, 3 ≤ N ≤ 20 3\le N\le20 3≤N≤20;
对于 100 % 100\% 100% 的数据, 3 ≤ N ≤ 40 , 1 ≤ X ≤ 7 , 1 ≤ Y ≤ 5 , 1 ≤ Z ≤ 5 3\le N\le40,1\le X\le7,1\le Y\le5,1\le Z\le5 3≤N≤40,1≤X≤7,1≤Y≤5,1≤Z≤5。
解题思路
这一把还没考 d p dp dp 呢,所以肯定是 d p dp dp(狗头)。
这一题比较有防 AK 的意思哈,观察数据范围,有可能是一个状态压缩 d p dp dp(实际上就是)。首先我们需要明确一点,计算满足条件的序列数量其实是不太好整的,因为一个序列里面可能出现好几个 “妙手”,那咋办呢?你计算不满足条件的序列数量不就行了嘛,然后拿全部的数量减一下就是答案。
然后我们设计状态 d p [ i ] [ j ] dp[i][j] dp[i][j],其中 i i i 表示前 i i i 个数字, j j j 表示一个压缩的状态。我们来详细说一下 j j j, j j j 是一个二进制数,表示的是前 i i i 个数有哪些后缀和。例如:序列 [ 1 , 2 , 3 , 4 ] [1, 2, 3, 4] [1,2,3,4],有 4 、 7 、 9 、 10 4、7、9、10 4、7、9、10(4,4 + 3,4 + 3 + 2,4 + 3 + 2 + 1) 一共 4 种后缀和,那么就让 j = 1101001000 j = 1101001000 j=1101001000(二进制,最右边一位表示后缀和 1 在不在)。那么 d p [ i ] [ j ] dp[i][j] dp[i][j] 就是序列的前 i i i 个数,后缀和满足 j j j 有多少种可能(只是前 i i i 个数字)。
那么怎么判断是不是我们想要的序列呢?假设 X = 3 , Y = 2 , Z = 1 X = 3,Y = 2,Z = 1 X=3,Y=2,Z=1,如果在 d p dp dp 的时候,发现同时存在后缀和 1 、 3 、 6 1、3、6 1、3、6 的话,是不是就能说明存在一个 “妙手” 呢?如果遇到这种情况我们不统计就可以了。
最后我们讨论一下如何进行状态转移,显然我们应该按照 i i i 从小到大的顺序进行转移,那么假设当前我们枚举到了序列的前 i i i 个,再枚举这一位是几,假设是 k k k,然后第三个数,我们枚举前 i − 1 i - 1 i−1 个数后缀和的情况,令其为 j j j。因为我们加上了一个 k k k,所以之前的后缀和都应该加上 k k k,等价于 j j j 左移 k k k 位,又因为 k k k 本身也算一个后缀和,所以再让 j j j 或上一个 ( 1 < < ( k − 1 ) ) (1 << (k - 1)) (1<<(k−1)) 表示 k k k 这个后缀和也存在,得到的值假设为 s s s,那么就有了下面的转移方程。
s = ( ( j < < k ) ∣ ( 1 < < ( k − 1 ) ) ) s = ((j << k) | (1 << (k - 1))) s=((j<<k)∣(1<<(k−1)))
d p [ i ] [ s ] = d p [ i ] [ s ] + d p [ i − 1 ] [ j ] dp[i][s] = dp[i][s] + dp[i - 1][j] dp[i][s]=dp[i][s]+dp[i−1][j]
因为 j j j 不需要储存大于 ( X + Y + Z ) (X+Y+Z) (X+Y+Z) 的后缀和(没意义,不能帮助我们确定是否存在 “妙手”),所以我们要保证 j ≤ ( 1 < < ( x + y + z ) ) − 1 j \le (1 << (x + y + z)) - 1 j≤(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 是为了保证去掉溢出的内容,让状态值小于 allif ((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;
}
本人能力有限,如有不当之处敬请指教!