2024年3月ZZUACM 招新赛题解

2024年3月ZZUACM 招新赛

题号题目
A区间次大值
B上课签到
C魔法森林(一)
D魔法森林(二)
ELOP
F跳格子
G猜数字
H抽卡记录
I安达的二维矩阵
J安达的数字手术
K跳楼梯
L前缀和

A 区间次大值—循环/签到题

题目描述

给定一个 n n n的全排列 a i a_i ai,下标为 1 − n 1-n 1n,请你输出所有 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 n103 1 ≤ a i ≤ n 1\leq a_i \leq n 1ain。如果 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 1m2104 1 ≤ a i , z , h ≤ 1 0 7 1 \le a_i,z, h \le 10^7 1ai,z,h107 1 ≤ x , y ≤ n 1 \le x,y \le n 1x,yn

题解

首先,对于最大…的最小,都可以考虑二分思路。其次,对于双权值问题,如果可以开二维,是可以跑二维的,但是明显这题开不了二维(会爆栈和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 long
 
const 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,m5000 1 ≤ l ≤ 1 0 6 1\leq l \leq 10^6 1l106

题解

按照指令字符串模拟将要走到的位置即可

可能的难点在于 大写字母的传送 模拟,可以用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,m5000

题解

最短路问题在路径权值相同时可以用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=5RRBBR后红色方率先赢得三比赛,拿下一,红色方胜利局数 1 1 1,之后的BRBRB中蓝色方率先赢得三比赛,拿下第二,蓝色方胜利局数 1 1 1,最后BBRB中,蓝色方率先赢得三比赛,胜利局数变为 2 2 2,赢得了本次比赛,成为获胜方。

数据范围与约定

数据保证 0 < ∣ S ∣ ≤ 1 0 6 0<|S|\leq 10^6 0<S106,且对局有效。

题解

多读几遍题,发现题目就是完全误导你往游戏规则上思考,但这个题与游戏规则完全无关

到最后一个字符才出现胜负,证明在最后一个字符之前还无法判断胜负,最后一个字符决定了胜负,所以直接判断最后一个字符即可

#include <iostream>
#include <cstring>

using namespace std;

signed main() {
    string s; cin >> s;
    cout << s.back() << endl;
} 

F 跳格子—dp

题目描述

阿昆(简称AKun)喜欢别人叫自己AK,更喜欢玩跳格子。 格子形状以及标号如下:

pic

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 4

1 2 4

1 3 4

数据范围与约定

( 2 ≤ n ≤ 1 0 6 ) (2 \le n\le 10^6) (2n106)

题解

经典的爬楼梯问题,可参考爬楼梯

因为只能朝右前方前进,因此每个数字的状态只能由他左边相邻的数字得到

即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 1xn

有一个随机数生成器(简称生成器),每次使用时会等概率生成在 [ l , r ]   ( l ≤ r ) [l,r]\ (l\leq r) [l,r] (lr) 范围内的一个正整数( [ 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 1 ,游戏直接结束。
  2. 生成了 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} 211+212=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 1xn5000

题解

这题考点比较多,比较吃基本功。知识点:期望+区间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 1n1000

0 ≤ k ≤ 10 0\leq k\leq 10 0k10

1 ≤ i ≤ n 1\leq i\leq n 1in

1 ≤ a i ≤ 100000 1\leq a_i \leq 100000 1ai100000

题解

先看数据范围,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 1n,m103 0 ≤ g [ i ] [ j ] ≤ 1 0\leq g[i][j] \leq 1 0g[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 1n106 n u m num num 仅由若干位数字(0 - 9)组成且除 0 0 0本身外, n u m num num不含任何前导零。

题解

先说结论,从左往右找到第一个a[i] > a[i+1]的位置把a[i]删除即可,如果不存在则删除最后一位

原理:(可参考此题目的升级版)移除K位数字

在这里插入图片描述

#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);
    // 除去前导0
    int 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 x1

假设这栋楼无限大(意味着有负无穷和正无穷层),试求到达第 n n n层的最小操作次数。

最开始飞云处在第 0 0 0层。

输入格式

第一行一个整数 t t t表示测试案例数 ( 1 ≤ t ≤ 1000 ) (1\le t \le 1000) (1t1000)

每一个测试案例读入一个整数 n n n表示楼层数。 ( 1 ≤ n ≤ 1 0 6 ) (1 \le n \le 10^6) (1n106)

输出格式

输出一行一个正整数表示答案

输入输出样例

样例输入 #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 1t1000,1n1e6.

题解

这题可以理解为 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 ai0

接下来 q q q次询问,每次询问给定 s u m i sum_i sumi,请你输出最小的 r r r,满足 1 ≤ r ≤ n 1\leq r\leq n 1rn a 1 + a 2 + . . . + a r ≥ s u m i a_1+a_2+...+a_r\geq sum_i a1+a2+...+arsumi,也就是 ∑ j = 1 r a j ≥ s u m i \sum_{j=1}^r a_j \geq sum_i j=1rajsumi。如果不存在这样的 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,q106 0 ≤ a i ≤ 1 0 6 0\leq a_i \leq 10^6 0ai106 0 ≤ s u m i ≤ 1 0 18 0\leq sum_i\leq 10^{18} 0sumi1018

题解

对于最大…的最小且存在单调性的问题,都可以考虑二分思路

单调性, 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';
    }
     
}
  • 24
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_WAWA鱼_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值