2024年3月ZZUACM 招新赛
题号 | 题目 |
---|---|
A | 区间次大值 |
B | 上课签到 |
C | 魔法森林(一) |
D | 魔法森林(二) |
E | LOP |
F | 跳格子 |
G | 猜数字 |
H | 抽卡记录 |
I | 安达的二维矩阵 |
J | 安达的数字手术 |
K | 跳楼梯 |
L | 前缀和 |
A 区间次大值—循环/签到题
题目描述
给定一个 n n n的全排列 a i a_i ai,下标为 1 − n 1-n 1−n,请你输出所有 l < r l<r l<r的区间 [ l , r ] [l,r] [l,r]中次大数之和。
n n n的全排列指一个长为 n n n的数组, 1 , 2... N 1,2...N 1,2...N每个数字出现且只出现一次。
输入格式
第一行一个整数表示 n n n
第二行 n n n个整数表示 n n n的全排列
输出格式
输出一行一个正整数表示答案
输入输出样例
样例输入 #1
4
4 1 3 2
样例输出 #1
12
样例解释 #1
区间 [ 1 , 2 ] [1,2] [1,2]的次大值为 1 1 1,区间 [ 1 , 3 ] [1,3] [1,3]的次大值为 3 3 3,区间 [ 1 , 4 ] [1,4] [1,4]的次大值为 3 3 3,区间 [ 2 , 3 ] [2,3] [2,3]的次大值为 1 1 1,区间 [ 2 , 4 ] [2,4] [2,4]的次大值为 2 2 2,区间 [ 3 , 4 ] [3,4] [3,4]的次大值为 2 2 2。因此答案为 1 + 3 + 3 + 1 + 2 + 2 = 12 1+3+3+1+2+2=12 1+3+3+1+2+2=12
数据范围与约定
数据保证 n ≤ 1 0 3 n\leq 10^3 n≤103, 1 ≤ a i ≤ n 1\leq a_i \leq n 1≤ai≤n。如果 i ≠ j i\neq j i=j,则 a i ≠ a j a_i \neq a_j ai=aj。
题解
先看数据范围可二层循环,可直接暴力,第一层循环代表左端点,第二层右端点,维护一个最大值和次大值即可
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main() {int n; cin >> n;vector<int> a(n + 1, 0);for(int i = 0; i < n; i ++) cin >> a[i];int res = 0;for(int i = 0; i < n - 1; i ++) {int max1 = max(a[i], a[i + 1]);int max2 = min(a[i], a[i + 1]);res += max2;for(int j = i + 2; j < n; j ++) { if(a[j] > max1) {max2 = max1;max1 = a[j];} else if(a[j] > max2) {max2 = a[j];}res += max2;}}cout << res << endl;
}
B 上课签到—二分+最短路
题目描述
某一天飞云从宿舍起床,但他上课快迟到了,他想要尽可能快的到达教室。他要在上课之前到达教室签到,换句话说,如果还有 h h h分钟上课,他必须要在小于等于 h h h分钟内到达教室。现在将宿舍,十字路口,教室抽象成一张无向图,含有 n n n个点和 m m m条边。由于路况的不同,每到达一个点都要消耗 a i a_i ai体力值(起始位置和终止位置也算),每经过一条边需要 w w w分钟。而对于飞云来说,由于体力可以恢复,所以他只需要知道路径上的最大体力消耗。现在飞云向聪明的你求助,在不迟到的情况下,所选路径中最大体力消耗的最小值是多少。
输入格式
第一行读入五个数 n , m , s t , e d , h n,m,st,ed, h n,m,st,ed,h(分别无向图的点数,边数,起始位置,终止位置,距离上课所剩的时间(单位:分钟))
接下来n行分别读入 n n n个数 a i a_i ai(每个点消耗的体力值)
接下来m行读入 x , y , w x,y,w x,y,w(分别代表无向边的两点和路上所花费的时间)
输出格式
输出一行代表最大消耗体力的最小值,若会迟到,则输出 − 1 -1 −1
输入输出样例
样例输入 #1
4 4 1 4 8
8
5
6
10
1 3 4
2 4 1
2 1 2
3 4 9
样例输出 #1
10
样例解释
只能选择路径1->2->4
,花费 3 3 3分钟,路径上最大体力消耗是到达 4 4 4的时候,花费 10 10 10.
数据范围与约定
$1 \le n \le 10^4 $, 1 ≤ m ≤ 2 ∗ 1 0 4 1 \le m\le 2*10^4 1≤m≤2∗104, 1 ≤ a i , z , h ≤ 1 0 7 1 \le a_i,z, h \le 10^7 1≤ai,z,h≤107, 1 ≤ x , y ≤ n 1 \le x,y \le n 1≤x,y≤n
题解
首先,对于最大…的最小,都可以考虑二分思路。其次,对于双权值问题,如果可以开二维,是可以跑二维的,但是明显这题开不了二维(会爆栈和TLE),并且有单调性质,所以考虑二分。
单调性质:设最大体力消耗是x,如果x满足条件(即可以找到一条路径,路径的权值和小于等于h并且点权值都小于等于x),那么大于x的也满足条件。所以考虑二分。
然后判断是不是满足跑一下dijkstra最短路即可。
#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
#define PII pair<int, int>
#define int long longconst int N = 1e4 + 10;
int a[N];struct edge {int v, w;
};
vector<edge> e[N];
int n, m, st, ed, h;
int dist[N];
int vis[N];bool check(int x) { // Dijkstra最短路for (int i = 1; i <= n; i++) {dist[i] = 1e18;vis[i] = 0;}priority_queue<PII, vector<PII>, greater<PII>> q;q.push({0, st});dist[st] = 0;if (a[st] > x) return false;while (q.size()) {auto now = q.top();q.pop();int u = now.second;if (vis[u]) continue;vis[u] = 1;for (auto t: e[u]) {int v = t.v, w = t.w;if (dist[v] > dist[u] + w && a[v] <= x) {dist[v] = dist[u] + w;q.push({dist[v], v});}}}return dist[ed] <= h;
}signed main() {cin >> n >> m >> st >> ed >> h;for (int i = 1; i <= n; i++) {cin >> a[i];}for (int i = 1; i <= m; i++) {int u, v, w; cin >> u >> v >> w;e[u].push_back({v, w});e[v].push_back({u, w});}int l = 1, r = 1e7 + 10;while (l < r) {int mid = l + r >> 1;if (check(mid)) r = mid;else l = mid + 1;}if (l == 1e7 + 10) cout << -1 << '\n';else cout << l << '\n';
}
C 魔法森林(一)—模拟
题目描述
小G来到了向往已久的魔法森林,并且想要探索其中的宝藏。
具体的,魔法森林可以看作是一个 n × m n\times m n×m的矩阵,其中矩阵的每个位置都可以用一对坐标表示,例如 ( i , j ) (i,j) (i,j)第 i i i行第 j j j列的位置。魔法森林中每个位置的具体信息可以用一个字符来表示:
若当前位置字符是’.',则表示当前位置为空地,小G可以自由行走。
若当前位置字符是’#',则表示当前位置为障碍,小G不可以通过。
另外作为魔法森林,魔法传送阵是其特色,具体的来说若字符是大写的英文字母,则表示当前位置为魔法传送阵,可以保证的是魔法传送阵都是成对存在的,也就是说某个英文字母表示的魔法传送阵一旦出现,一定会在魔法森林中出现两个,且只会出现两个。所以,对应于 26 26 26个大写的英文字母,魔法森林中也最多出现 26 26 26对传送阵,相应的传送阵之间是有强制传送的功能的,也就是说,当小G走到某个魔法传送阵时,会被自动传送到该传送阵所对应的字母的另一个传送阵的位置。(注意只有当从其他位置走向传送阵所在的格子时,魔法传送阵才会生效。)
已知,小G无法进入障碍所在的位置,也无法走出魔法森林(即如果小G下一步要到的位置是障碍或要超出边界时,则小G会提供留在原地)。
现给定魔法森林分布情况,小G的起始位置和将要行走的指令(一个只包含 L , R , U , D L,R,U,D L,R,U,D的字符串,分别表示向左,右,上,下的指令)。请回答小G最后所在的位置。
PS:请注意,传送阵是强制性传送的。
输入格式
第一行两个由空格分隔的整数表示 n n n和 m m m,分别表示魔法森林的行数和列数。
接下来的 n n n行,每行一个长为 m m m的字符串,代表该魔法森林的情况说明。
接下来的一行,两个由空格分隔的整数表示 x x x和 y y y,分别表示小G初始所在的位置。(数据保证,小G初始时所在的位置一定是’.')
接下来的一行,一个整数 l l l,表示接下来下小G需要行走的指令数。
最后一行,有一个长度为 l l l的字符串构成,代表小G接受到的指令数。
输出格式
一行两个整数,表示小G最终所在的位置。
输入输出样例
样例输入 #1
3 3
.#.
A.A
#.#
1 1
5
RDLRD
样例输出 #1
2 1
数据范围与约定
数据保证 n , m ≤ 5000 n,m\leq 5000 n,m≤5000, 1 ≤ l ≤ 1 0 6 1\leq l \leq 10^6 1≤l≤106。
题解
按照指令字符串模拟将要走到的位置即可
可能的难点在于 大写字母的传送 模拟,可以用vector把2个传送坐标记录下来,每次模拟下一个走到的位置时,如果遇到大写字母(传送阵),从vector中找到与当前坐标不同的另一个坐标即为传送后的位置,模拟即可
#include <iostream>
#include <cstring>
#include <vector>using namespace std;
const int N = 5010;
char g[N][N];
vector<pair<int,int>> vec[30];
signed main() {int n, m; cin >> n >> m;for(int i = 1; i <= n; i ++) {for(int j = 1; j <= m; j ++) {cin >> g[i][j];if(g[i][j] >= 'A' && g[i][j] <= 'Z') {vec[g[i][j] - 'A'].push_back({i, j}); }}}int x, y; cin >> x >> y;int k; cin >> k;string s; cin >> s;for(int i = 0; i < k; i ++) {int tox = x, toy = y;if(s[i] == 'L') toy --;else if(s[i] == 'R') toy ++;else if(s[i] == 'U') tox --;else tox ++;if(tox < 1 || toy < 1 || tox > n || toy > m) continue;if(g[tox][toy] == '#') continue;if(g[tox][toy] == '.') {x = tox; y = toy;} else { // 传送int pos = g[tox][toy] - 'A';if(tox == vec[pos].front().first && toy == vec[pos].front().second) {tox = vec[pos].back().first;toy = vec[pos].back().second;} else {tox = vec[pos].front().first;toy = vec[pos].front().second;} x = tox; y = toy;}}cout << x << " " << y << endl;
}
D 魔法森林(二)—bfs
题目描述
小G来到了向往已久的魔法森林,并且想要探索其中的宝藏。
具体的,魔法森林可以看作是一个 n × m n\times m n×m的矩阵,其中矩阵的每个位置都可以用一对坐标表示,例如 ( i , j ) (i,j) (i,j)第 i i i行第 j j j列的位置。魔法森林中每个位置的具体信息可以用一个字符来表示:
若当前位置字符是’.',则表示当前位置为空地,小G可以自由行走。
若当前位置字符是’#',则表示当前位置为障碍,小G不可以通过。
另外作为魔法森林,魔法传送阵是其特色,具体的来说若字符是大写的英文字母,则表示当前位置为魔法传送阵,可以保证的是魔法传送阵都是成对存在的,也就是说某个英文字母表示的魔法传送阵一旦出现,一定会在魔法森林中出现两个,且只会出现两个。所以,对应于 26 26 26个大写的英文字母,魔法森林中也最多出现 26 26 26对传送阵,相应的传送阵之间是有强制传送的功能的,也就是说,当小G走到某个魔法传送阵时,会被自动传送到该传送阵所对应的字母的另一个传送阵的位置。(注意只有当从其他位置走向传送阵所在的格子时,魔法传送阵才会生效。)
已知,小G无法进入障碍所在的位置,也无法走出魔法森林(即如果小G下一步要到的位置是障碍或要超出边界时,则小G会提供留在原地)。
现给定魔法森林的情况,小G所在的起点和终点,如果小G每次移动都将耗费一点体力,且传送阵并不消耗体力,请问小G要从起点到终点最少耗费的体力数?(若无法到达则输出-1)
PS:请注意,传送阵是强制性传送的。
输入格式
第一行两个由空格分隔的整数表示 n n n和 m m m,分别表示魔法森林的行数和列数。
接下来的 n n n行,每行一个长为 m m m的字符串,代表该魔法森林的情况说明。
接下来的一行,四个由空格分隔的整数表示 x 1 x_1 x1和 y 1 y_1 y1, x 2 x_2 x2和 y 2 y_2 y2分别表示小G的起点和终点。(数据保证,小G起点和终点的位置一定是’.')。
输出格式
一行一个整数,表示小G所需耗费的最少体力数,若无法到达则输出-1。
输入输出样例
样例输入 #1
3 3
.#..
A..A
#.#.
1 1 2 2
样例输出 #1
3
数据范围与约定
数据保证 n , m ≤ 5000 n,m\leq 5000 n,m≤5000。
题解
最短路问题在路径权值相同时可以用bfs解决,可参考经典题目
这个题在经典题目的基础上多了一个传送阵,可采用与上一题一样的方式模拟下一步走到的位置
#include <iostream>
#include <cstring>
#include <vector>
#include <queue>using namespace std;
const int N = 5010;
char g[N][N];
int vis[N][N];
int dist[N][N];
vector<pair<int,int>> vec[30];
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
signed main() {memset(dist, 0x3f, sizeof dist);memset(vis, 0, sizeof vis);int n, m; cin >> n >> m;for(int i = 1; i <= n; i ++) {for(int j = 1; j <= m; j ++) {cin >> g[i][j];if(g[i][j] >= 'A' && g[i][j] <= 'Z') {vec[g[i][j] - 'A'].push_back({i, j}); }}}int x1, y1; cin >> x1 >> y1;int x2, y2; cin >> x2 >> y2;queue<pair<int,int>> q;q.push({x1, y1});dist[x1][y1] = 0;while(q.size()) {int x = q.front().first;int y = q.front().second;q.pop();if(vis[x][y]) continue;vis[x][y] = 1;for(int i = 0; i < 4; i ++) {int tox = x + dx[i], toy = y + dy[i];if (tox < 1 || toy < 1 || tox > n || toy > m) continue;if (vis[tox][toy] || g[tox][toy] == '#') continue;if (g[tox][toy] != '.') {int pos = g[tox][toy] - 'A';if(tox == vec[pos].front().first && toy == vec[pos].front().second) {tox = vec[pos].back().first;toy = vec[pos].back().second;} else {tox = vec[pos].front().first;toy = vec[pos].front().second;} }dist[tox][toy] = min(dist[tox][toy], dist[x][y] + 1);q.push({tox, toy});}}if(dist[x2][y2] == 0x3f3f3f3f) cout << -1 << endl;else cout << dist[x2][y2] << endl;
}
E LOP—签到题
题目描述
P19E99最近迷上了一款叫猪蛋联盟(League of Pigegg)的游戏,他决定成为职业选手,于是开始看职业联赛。
在这里首先向大家介绍一下Best of x赛制,即BO x赛制:每次比赛最多进行 x x x (保证 x x x为奇数) 局,最先赢得 ⌈ x 2 ⌉ \lceil\frac{x}{2}\rceil ⌈2x⌉ (即 x x x除以 2 2 2取上整) 局的队伍获胜。
League of Pigegg这款游戏的比赛中,比赛分为两方:红色方与蓝色方,每次比赛采取BO n赛制,即每次比赛最多进行 n n n 局,最先赢得 ⌈ n 2 ⌉ \lceil\frac{n}{2}\rceil ⌈2n⌉ 局的队伍获胜。而每一局的胜负判定又采用BO m赛制,即每局最多进行 m m m 把,最先赢得 ⌈ m 2 ⌉ \lceil\frac{m}{2}\rceil ⌈2m⌉ 把的队伍拿下本局的胜利。
而P19E99是个大笨蛋,他即不知道 n n n 等于多少,也不知道 m m m 等于多少,作为一名注重结果的小朋友,他只知道每把比赛哪一方获得了胜利,而作为一名伸手党,他想问你究竟哪一方获得了本次比赛的胜利。
输入格式
一行一个字符串 S S S,只包括 ‘R’ 和 ‘B’ ,代表每把比赛的获胜方。
输出格式
一行一个字符 ‘R’ 或 ‘B’,代表本次比赛的获胜方
输入输出样例
样例输入 #1
RRBBRBRBRBBBRB
样例输出 #1
B
样例输入 #2
RRBRBRBBBRBBRB
样例输出 #2
B
样例解释
在样例 #1中, n = 3 , m = 5 n=3,\ m=5 n=3, m=5,RRBBR后红色方率先赢得三把比赛,拿下一局,红色方胜利局数为 1 1 1,之后的BRBRB中蓝色方率先赢得三把比赛,拿下第二局,蓝色方胜利局数为 1 1 1,最后BBRB中,蓝色方率先赢得三把比赛,胜利局数变为 2 2 2,赢得了本次比赛,成为获胜方。
数据范围与约定
数据保证 0 < ∣ S ∣ ≤ 1 0 6 0<|S|\leq 10^6 0<∣S∣≤106,且对局有效。
题解
多读几遍题,发现题目就是完全误导你往游戏规则上思考,但这个题与游戏规则完全无关
解法一:到最后一个字符才出现胜负,证明在最后一个字符之前还无法判断胜负,最后一个字符决定了胜负,所以直接判断最后一个字符即可
#include <iostream>
#include <cstring>using namespace std;signed main() {string s; cin >> s;cout << s.back() << endl;
}
解法二:胜者的条件是 每局胜的把数最多和总体胜的局数最多,因此胜利的一定是字符出现最多的那个
#include <iostream>
#include <cstring>using namespace std;signed main() {string s; cin >> s;int sumr = 0, sumb = 0;for(int i = 0; i < s.size(); i ++) {sumr += s[i] == 'R';sumb += s[i] == 'B';} if(sumr > sumb) cout << "R" << endl;else cout << "B" << endl;
}
F 跳格子—dp
题目描述
阿昆(简称AKun)喜欢别人叫自己AK,更喜欢玩跳格子。 格子形状以及标号如下:
Akun每次跳格子只能向着右方前进,即他脚下的格子标号只会不断增大,并且每步只能跳到相邻的格子上。
格子是无穷无尽的,永远没有终点。但AKun的体力有耗尽的时候,因此他给自己定了一个小目标,他认为只要达到了 n n n号格子就是胜利。 现在问AKun从1号格子出发,共有多少种方案能到达 n n n号格子。
Akun知道方案可能太多太多,所以他想让你输出总数量取模 1 0 9 + 7 10^9+7 109+7 之后的结果。
一个数 x x x对 y y y取模即 求 x x x整除以 y y y之后的余数 x % y x\%y x%y。
输入格式
输入一行一个数字 n n n . 代表Akun最终要到达的格子的标号。
输出格式
输出一行一个数字 a n s ans ans, 代表方案数取模 1 0 9 + 7 10^9+7 109+7之后的结果。
输入输出样例
样例输入 #1
4
样例输出 #1
3
样例输入 #2
987654
样例输出 #2
530848436
样例解释
在样例#1中,有如下三种方案:
1 2 3 41 2 41 3 4
数据范围与约定
( 2 ≤ n ≤ 1 0 6 ) (2 \le n\le 10^6) (2≤n≤106)
题解
经典的爬楼梯问题,可参考
因为只能朝右前方前进,因此每个数字的状态只能由他左边相邻的数字得到
即f[i] = f[i - 1] + f[i - 2], f[i]表示第i个数字的方案数
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int mod = 1e9 + 7;
signed main() {int n; cin >> n;vector<int> f(n + 1, 0);f[1] = 1; f[2] = 1;for(int i = 3; i <= n; i ++) {f[i] = (f[i - 1] + f[i - 2]) % mod;}cout << f[n] << endl;
}
G 猜数字—期望+区间dp + 前缀和优化 + 乘法逆元
题目描述
这里有一个有趣的猜数字游戏。
给出 n n n 和一个小于等于 n n n 的正整数 x x x 。玩家不知道 x x x 具体是多少,只知道 n n n 且 1 ≤ x ≤ n 1\leq x\leq n 1≤x≤n 。
有一个随机数生成器(简称生成器),每次使用时会等概率生成在 [ l , r ] ( l ≤ r ) [l,r]\ (l\leq r) [l,r] (l≤r) 范围内的一个正整数( [ l , r ] [l,r] [l,r] 叫做生成范围), l l l 和 r r r 在每次使用前由玩家自己选择,它们均为正整数。
玩家要使用生成器生成 x x x ,每次使用生成器生成一个数后(假设生成了 y y y ),如果 x = y x=y x=y 则游戏结束。
否则玩家会知道到 x x x 和 y y y 的大小关系(大于或小于),玩家想要尽可能少的使用生成器,请问游戏结束前玩家使用生成器的期望次数是多少。
同时玩家是一个非常谨慎的人,如果他不能确定一个数是否为 x x x ,他就会把这个数包含在生成范围内。
答案对 998244353 998244353 998244353 取模。
输入格式
输入一行两个整数,分别表示 n , x n,x n,x 。
输出格式
输出一行一个正整数表示对 998244353 998244353 998244353 取模后的答案。
输入输出样例
样例输入 #1
2 1
样例输出 #1
499122178
样例解释 #1
一开始玩家将生成器范围设置为 [ 1 , 2 ] [1,2] [1,2] ,使用后有两种情况。
- 生成了 1 1 1 ,游戏直接结束。
- 生成了 2 2 2 ,玩家知道了 x x x 小于 2 2 2 ,之后将生成器范围设置为 [ 1 , 1 ] [1,1] [1,1] ,再次使用只会生成 1 1 1 ,游戏结束。
两种情况出现的概率均为 1 2 \frac{1}{2} 21 ,总使用次数的期望为 1 2 ∗ 1 + 1 2 ∗ 2 = 3 2 \frac{1}{2}*1+\frac{1}{2}*2=\frac{3}{2} 21∗1+21∗2=23 , 3 2 \frac{3}{2} 23 对 998244353 998244353 998244353 取模后为 499122178 499122178 499122178 。
样例输入 #2
2024 3
样例输出 #2
792613284
数据范围与约定
数据保证 1 ≤ x ≤ n ≤ 5000 1\leq x\leq n\leq 5000 1≤x≤n≤5000 。
题解
这题考点比较多,比较吃基本功。知识点:期望+区间dp + 前缀和优化 + 乘法逆元
对于期望问题,我一般令终态的状态作为起始状态,比如这题我让f[x][x]: 在[x,x]区间中选出x的期望。然后由小区间扩展到大区间,进行区间dp。
设区间f[i][j]: 区间[i,j]最终拿到x的期望。然后画一个数轴可以知道分三种情况套路。
1.如果拿的数是x,那么f[i][j] += 1 / (j - i + 1)
2.如果拿的数小于x,那么f[i][j] += ∑ (f[k][j] + 1)/ (j - i + 1) (k从i + 1 到 x)
3.如果拿的数大于x,那么f[i][j] += ∑(f[i][k] + 1) / (j - i + 1) (k从x到 j - 1)
对于∑这部分,可以发现对于同一个i和同一个j的状态来说可以提前保存下来,可以看我代码注释的部分,这是一个n^3的写法,更易理解。
最后就是乘法逆元,解决除法取模的问题,如果不知道可以去学习一下,这里我直接用的大数取余的模板。
#include <iostream>
#include <algorithm>
using namespace std;template<const int T>
struct ModInt {const static int mod = T;int x;ModInt(int x = 0) : x(x % mod) {}ModInt(long long x) : x(int(x % mod)) {}int val() { return x; }ModInt operator + (const ModInt &a) const { int x0 = x + a.x; return ModInt(x0 < mod ? x0 : x0 - mod); }ModInt operator - (const ModInt &a) const { int x0 = x - a.x; return ModInt(x0 < 0 ? x0 + mod : x0); }ModInt operator * (const ModInt &a) const { return ModInt(1LL * x * a.x % mod); }ModInt operator / (const ModInt &a) const { return *this * a.inv(); }bool operator == (const ModInt &a) const { return x == a.x; };bool operator != (const ModInt &a) const { return x != a.x; };void operator += (const ModInt &a) { x += a.x; if (x >= mod) x -= mod; }void operator -= (const ModInt &a) { x -= a.x; if (x < 0) x += mod; }void operator *= (const ModInt &a) { x = 1LL * x * a.x % mod; }void operator /= (const ModInt &a) { *this = *this / a; }friend ModInt operator + (int y, const ModInt &a){ int x0 = y + a.x; return ModInt(x0 < mod ? x0 : x0 - mod); }friend ModInt operator - (int y, const ModInt &a){ int x0 = y - a.x; return ModInt(x0 < 0 ? x0 + mod : x0); }friend ModInt operator * (int y, const ModInt &a){ return ModInt(1LL * y * a.x % mod);}friend ModInt operator / (int y, const ModInt &a){ return ModInt(y) / a;}friend ostream &operator<<(ostream &os, const ModInt &a) { return os << a.x;}friend istream &operator>>(istream &is, ModInt &t){return is >> t.x;}ModInt pow(int64_t n) const {if(n == 0) return 1;ModInt res(1), mul(x);while(n){if (n & 1) res *= mul;mul *= mul;n >>= 1;}return res;}ModInt inv() const {int a = x, b = mod, u = 1, v = 0;while (b) {int t = a / b;a -= t * b; swap(a, b);u -= t * v; swap(u, v);}if (u < 0) u += mod;return u;}};
using mint = ModInt<998244353>;
const int N = 5e3 + 10;
mint fact[N], invfact[N];
void init(){fact[0] = invfact[0] = 1;for(int i = 1; i < N; i ++) fact[i] = fact[i - 1] * i;invfact[N - 1] = fact[N - 1].inv();for(int i = N - 2; i; i --)invfact[i] = invfact[i + 1] * (i + 1);
}
inline mint C(int a, int b){if (a < 0 || b < 0 || a < b) return 0;return fact[a] * invfact[b] * invfact[a - b];
}mint f[N][N];
mint s[N][N];
mint add[N];signed main() {int n, x; cin >> n >> x;f[x][x] = 0;mint t2 = 0;for (int i = x; i >= 1; i--) {mint t1 = 0;for (int j = x; j <= n; j++) {f[i][j] += (t1 + j - x) / mint(j - i + 1);f[i][j] += (add[j] + x - i) / mint(j - i + 1);f[i][j] += 1 / (mint)(j - i + 1); t1 += f[i][j];add[j] += f[i][j];}}cout << f[1][n] <<'\n';
}
H 抽卡记录—dp
题目描述
HS的抽卡记录可以用一个长度为 n n n的数列表示出来,序列的第 i i i个数 a i a_i ai表示第 i i i次出货用了 a i a_i ai抽,由于抽的次数比较多,HS的心情会有一定的波动,当一段连续的抽卡中,每次出货用的抽数严格递增时,HS会感到开心;每次出货用的抽数严格递减时,HS会感到难过。现在HS想逆天改命,试图从原先的抽卡记录中删掉若干个数,得到新的抽卡记录数列 b i b_i bi使HS心情改变的总次数,即开心变难过或者难过变开心的总次数不超过 k k k次,由于HS有强迫症,所以不希望新数列任意相邻的两个数相同,求数列 b i b_i bi的最长长度。
输入格式
第一行两个整数 n n n, k k k。
第二行 n n n个整数 a i a_i ai。
输出格式
一个数表示所求数列的最长长度。
样例输入1
5 0
1 2 5 3 4
样例输出1
4
样例输入2
5 2
1 2 5 3 4
样例输出2
5
数据范围与约定
1 ≤ n ≤ 1000 1\leq n\leq1000 1≤n≤1000
0 ≤ k ≤ 10 0\leq k\leq 10 0≤k≤10
1 ≤ i ≤ n 1\leq i\leq n 1≤i≤n
1 ≤ a i ≤ 100000 1\leq a_i \leq 100000 1≤ai≤100000
题解
先看数据范围,n <= 1e3且k <= 10。如果你写过最长上升子序列这个板子就知道这题大概率是dp。
考虑f[i][j][k]:以i结尾的子序列,上升转下降,下降转上升的次数不超过j次,且最后是以上升结尾或者下降结尾(0表示上升,1表示下降)的子序列最大长度。
第一层循环直接遍历数组即可,第二层循环遍历上升转下降,下降转上升的次数(有种类似背包的思路),第三层循环遍历前i个数的下标。
dp方程看代码自行理解。
这题难点不在于dp的递推式推导,在于dp状态的设计(状态有点多,需要理清楚)。
#include <iostream>
using namespace std;const int N = 1e3 + 10;
int f[N][N][2];
int a[N];signed main() {int n, k; cin >> n >> k;for (int i = 1; i <= n; i++) cin >> a[i];int ans = 0;for (int i = 1; i <= n; i++) {for (int j = 0; j <= k; j++) {f[i][j][0] = f[i][j][1] = 1;for (int h = 1; h < i; h++) {if (a[i] > a[h]) {f[i][j][1] = max(f[i][j][1], f[h][j][1] + 1);if (j >= 1) {f[i][j][1] = max(f[i][j][1], f[h][j - 1][0] + 1);}}if (a[i] < a[h]) {f[i][j][0] = max(f[i][j][0], f[h][j][0] + 1);if (j >= 1) {f[i][j][0] = max(f[i][j][0], f[h][j - 1][1] + 1);}}}ans = max(ans, f[i][j][0]);ans = max(ans, f[i][j][1]);}}cout << ans << '\n';
}
I 安达的二维矩阵—签到题
题目描述
给你一个大小为 n × m n \times m n×m的二进制矩阵 g g g ,请你找出包含最多1的行的下标以及这一行中1的数目。
如果有多行包含最多的1,只需要选择 行下标最小 的那一行。
输入格式
第一行两个由空格分隔的整数表示 n n n和 m m m
接下来 n n n行,每行 m m m个整数,代表矩阵的元素
输出格式
输出 2 2 2个数,分别是包含最多1的行的下标和这一行中1的数目,中间用一个空格隔开。
输入输出样例
样例输入 #1
3 3
1 0 1
1 1 1
0 1 1
样例输出 #1
2 3
样例输入 #2
2 2
1 0
0 1
样例输出 #2
1 1
数据范围与约定
数据保证 1 ≤ n , m ≤ 1 0 3 1\leq n,m\leq 10^3 1≤n,m≤103, 0 ≤ g [ i ] [ j ] ≤ 1 0\leq g[i][j] \leq 1 0≤g[i][j]≤1。
题解
全场最简单的题,没什么好说的,直接遍历记录最大值即可
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main() {int g[1010][1010] = {0};int n, m; cin >> n >> m;for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) cin >> g[i][j];int ans = 0, ans_x = 0, sum = 0;for(int i = 1; i <= n; i ++) {sum = 0;for(int j = 1; j <= m; j ++)if(g[i][j] == 1) ++ sum;if(sum > ans) {ans = sum;ans_x = i;}}cout << ans_x << " " << ans << endl;
}
J 安达的数字手术—贪心
题目描述
给你一个长度为 n n n的以字符串表示的非负整数 n u m num num ,移除这个数中的 1 1 1 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
如果所有数字都被移除则输出 0 0 0
如果删除的数字是第一位,但第二位是0,则结果把第二位0去掉,即结果不含前导0
如9011,答案为11
输入格式
第一行表示字符串长度 n n n
第二行表示长度为 n n n的数字字符串
输出格式
输出 1 1 1行,表示移除 1 1 1位数字后最小的数字
样例输入 #1
5
29833
样例输出 #1
2833
样例输入 #2
1
9
样例输出 #2
0
数据范围与约定
数据保证 1 ≤ n ≤ 1 0 6 1\leq n \leq 10^6 1≤n≤106, n u m num num 仅由若干位数字(0 - 9)组成且除 0 0 0本身外, n u m num num不含任何前导零。
题解
先说结论,从左往右找到第一个a[i] > a[i+1]的位置把a[i]删除即可,如果不存在则删除最后一位
原理:(可参考此题目的升级版)
#include <iostream>
#include <cstring>
#include <algorithm>using namespace std;int main() {int n; cin >> n;string s; cin >> s;string res = "";for(int i = 0; i < s.size(); i ++) {if(s[i] > s[i + 1]) {res = s.substr(0, i) + s.substr(i + 1);break;}}if(res.size() == 0) res = s.substr(0, s.size() - 1);// 除去前导0int pos = 0;while(res[pos] == '0' && pos < n) pos ++;res = res.substr(pos);// 特判 10000 和 个位数情况if(n == 1 || res.size() == 0) res = "0";cout << res << endl;
}
K 跳楼梯—找规律
题目描述
飞云现在终于到达了教室,他需要爬 n n n层楼梯,假设他现在处于第 x x x层,正在进行第 k k k次操作,可以做出以下两种操作之一:
- 移动到 x + k x+k x+k层
- 移动到 x − 1 x -1 x−1层
假设这栋楼无限大(意味着有负无穷和正无穷层),试求到达第 n n n层的最小操作次数。
最开始飞云处在第 0 0 0层。
输入格式
第一行一个整数 t t t表示测试案例数 ( 1 ≤ t ≤ 1000 ) (1\le t \le 1000) (1≤t≤1000)
每一个测试案例读入一个整数 n n n表示楼层数。 ( 1 ≤ n ≤ 1 0 6 ) (1 \le n \le 10^6) (1≤n≤106)
输出格式
输出一行一个正整数表示答案
输入输出样例
样例输入 #1
5
1
2
3
4
5
样例输出 #1
1
3
2
3
4
样例解释
在第一个测试案例,只需要1次,从0跳到1.
在第二个测试案例,至少需要三次。
第一次,从0跳到1.
第二次,从1跳到3
第三次,从3跳到2。
数据范围与约定
数据保证 1 ≤ t ≤ 1000 , 1 ≤ n ≤ 1 e 6 1 \le t \le 1000, 1 \le n \le 1e6 1≤t≤1000,1≤n≤1e6.
题解
这题可以理解为 1+2+3+4+…+k(代表k次操作)一直加到大于等于n后,可以把其中任意几位数字变为-1加起来使其等于n
我们会发现一个规律 这k个数之和sum,一定可以在k次操作后凑出 1+2+3+…+k-1之和 + 1 到 sum - 2
为什么不是sum-1呢,因为我们把1变为-1后,相当于-2了
比如 28 = 1 + 2 + 3 + 4 + 5 + 6 + 7
26 可以由 -1 + 2 + 3 + 4 + 5 + 6 + 7,7次操作即可
25 可以由 1 + -1 + 3 + 4 + 5 + 6 + 7,7次操作
24 可以由 1 + 2 + -1 + 4 + 5 + 6 + 7,7次操作
但27 因为大于 sum - 2 所以不可以只能由8次操作
如 1 + 2 + 3 + 4 + 5 + 6 + 7 + -1,8次操作组成
#include <iostream>
#include <cstring>
using namespace std;void solve() {int n; cin >> n;int res, sum = 0;for(int i = 1; i <= 2000; i ++) {sum += i;if(sum == n || sum - 1 > n) {res = i;break;}}cout << res << endl;
}signed main() {int T; cin >> T;while(T --) solve();
}
L 前缀和-二分+前缀和
题目描述
给定一个长为 n n n的数组 a i a_i ai,保证 a i ≥ 0 a_i\geq 0 ai≥0。
接下来 q q q次询问,每次询问给定 s u m i sum_i sumi,请你输出最小的 r r r,满足 1 ≤ r ≤ n 1\leq r\leq n 1≤r≤n且 a 1 + a 2 + . . . + a r ≥ s u m i a_1+a_2+...+a_r\geq sum_i a1+a2+...+ar≥sumi,也就是 ∑ j = 1 r a j ≥ s u m i \sum_{j=1}^r a_j \geq sum_i ∑j=1raj≥sumi。如果不存在这样的 r r r,请输出 − 1 -1 −1。
输入格式
第一行两个由空格分隔的整数表示 n n n和 q q q
第二行 n n n个整数表示数组 a i a_i ai
接下来 q q q行,每行一个整数表示一次询问。
输出格式
输出 q q q行,对于第 i i i行,如果有答案,请输出最小的满足要求的 r r r;如果没有答案,请输出 − 1 -1 −1。
输入输出样例
样例输入 #1
3 7
1 2 3
0
1
2
3
4
5
6
7
样例输出 #1
1
1
2
2
3
3
3
-1
数据范围与约定
数据保证 n , q ≤ 1 0 6 n,q\leq 10^6 n,q≤106, 0 ≤ a i ≤ 1 0 6 0\leq a_i \leq 10^6 0≤ai≤106, 0 ≤ s u m i ≤ 1 0 18 0\leq sum_i\leq 10^{18} 0≤sumi≤1018。
题解
对于最大…的最小且存在单调性的问题,都可以考虑二分思路
单调性, a 1 + a 2 + . . . + a r a_1+a_2+...+a_r a1+a2+...+ar 一定是越加越大,因此直接二分答案即可
#include <iostream>
#include <cstring>
#include <vector>using namespace std;
signed main() {int n, q; scanf("%d %d",&n, &q);vector<int> a(n + 1, 0);vector<long long> s(n + 1, 0);for(int i = 1; i <= n; i ++) {scanf("%d", &a[i]);s[i] = s[i - 1] + a[i];}int l, r;long long sum;while(q --) {l = 1, r = n;scanf("%lld", &sum);while(l < r) {int mid = l + r >> 1;if(s[mid] >= sum) r = mid;else l = mid + 1; }if(s[n] < sum) cout << -1 << '\n';else cout << r << '\n';}}