A.2023(字符串)
题意:
给出一个以2023
为结尾的字符串,要求你把该字符串的结尾修改为2024
。
分析:
本题解法较多,可以先输出前
s
.
l
e
n
g
t
h
(
)
−
1
s.length() - 1
s.length()−1个字符,然后单独输出4
,也可以修改最后一个字符,再整体输出。
代码:
#include<bits/stdc++.h>
using namespace std;
int main(){
string s;
cin >> s;
s[s.size() - 1] = '4';
cout << s << endl;
return 0;
}
B.Tetrahedral Number(枚举)
题意:
给出一个数字 N N N,要求输出所有满足 x + y + z ≤ N x + y + z \le N x+y+z≤N的三元组 ( x , y , z ) (x, y, z) (x,y,z)
分析:
使用三层循环枚举三元组即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
cin >> n;
for (int i = 0; i <= n; i++) {
for (int j = 0; i + j <= n; j++) {
for (int k = 0; i + j + k <= n; k++) {
cout << i << ' ' << j << ' ' << k << endl;
}
}
}
return 0;
}
C.Loong Tracking(思维)
题意:
有 n n n个点,开始时第 i ( i = 1 , 2 , . . . , n ) i(i = 1, 2, ..., n) i(i=1,2,...,n)个点的位置为 ( i , 0 ) (i, 0) (i,0)。
题目将给出 Q Q Q个操作,操作分为以下两种:
-
1 C
: 将序号为1
的点按C
方向移动一格,且所有序号为 j j j的格子也会随着移动,即序号为 j ( j = 2 , 3 , . . . , n ) j(j = 2, 3, ..., n) j(j=2,3,...,n)的格子会来到操作前序号为 j − 1 j - 1 j−1的格子所在的位置,其中 C ∈ ( R , L , U , D ) C \in (R, L, U, D) C∈(R,L,U,D),且 ( R , L , U , D ) (R, L, U, D) (R,L,U,D)分别代表右,左,上,下。 -
2 p
: 找到当前序号为 p p p的点所在的位置,并将该位置输出。
分析:
如果按要求进行模拟,那么时间复杂度就会来到 O ( N Q ) O(NQ) O(NQ),无法通过本题。
观察样例后,可以想到,既然移动的永远只有序号为
1
1
1的点,那么可以先将所有点所在的位置存在vector
中,然后在每次移动操作后将序号为
1
1
1的点当前所在的位置也存入vector
中,那么对于此时对于每个查询,均有一个长度为
n
+
q
(
q
为移动次数
)
n + q(q\text{为移动次数})
n+q(q为移动次数)的vector
,且所有点均会沿着前面的点走过的路径进行移动,因此,可以通过序号+移动次数直接得到当前位置所在的下标。
为了便于处理,将序号倒着存放,此时序号为 i i i的点开始时所在的下标为 n − i ( 下标从0开始存放 ) n - i(\text{下标从0开始存放}) n−i(下标从0开始存放),每次询问需要输出的元素对应的下标为 n − i + c n t n - i + cnt n−i+cnt,其中 c n t cnt cnt为移动操作的次数。
代码:
#include<bits/stdc++.h>
using namespace std;
int n, q;
vector<int> x, y;
void init() {//初始化,将开始时所有点的坐标放入vector
for (int i = n; i >= 1; i--) {
x.push_back(i);
y.push_back(0);
}
}
int main(){
cin >> n >> q;
init();
int cnt = 0;
while (q--) {
int op;
cin >> op;
if (op == 1) {
char c;
cin >> c;
if (c == 'U') {
x.push_back(x.back());
y.push_back(y.back() + 1);
} else if (c == 'D') {
x.push_back(x.back());
y.push_back(y.back() - 1);
} else if (c == 'L') {
x.push_back(x.back() - 1);
y.push_back(y.back());
} else {
x.push_back(x.back() + 1);
y.push_back(y.back());
}
cnt++;
} else {
int id;
cin >> id;
cout << x[n - id + cnt] << ' ' << y[n - id + cnt] << endl;
}
}
return 0;
}
D.Loong and Takahashi(构造)
题意:
给出一个 N × N N \times N N×N的网格,其中 N N N为奇数,你需要按以下要求在网格内填充内容:
-
字母
T
必须在网格正中间。 -
任意一个其他的网格均需填下数字 ( 1 ∼ N 2 − 1 ) (1 \sim N^{2} - 1) (1∼N2−1)。
-
所有填在网格中的数字,需满足值为 i i i所在的格子与值为 i + 1 i + 1 i+1的网格相邻(四方向)。
分析:
按要求进行模拟,从最外圈开始进行右,下,左,上四方向填数即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int n, ans[50][50];
int check(int x, int y) {
if (x < 1 || x > n || y < 1 || y > n || ans[x][y]) return 0;
return 1;
}
int main(){
cin >> n;
int x = 1, y = 1, cnt = 1;
while (cnt < n * n) {
while (check(x, y)) {
ans[x][y++] = cnt++;
}
y--;
x++;
while (check(x, y)) {
ans[x++][y] = cnt++;
}
x--;
y--;
while (check(x, y)) {
ans[x][y--] = cnt++;
}
y++;
x--;
while (check(x, y)) {
ans[x--][y] = cnt++;
}
x++;
y++;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (j != 1) cout << ' ';
if (ans[i][j] == 0) cout << 'T';
else cout << ans[i][j];
}
cout << endl;
}
return 0;
}
E - Non-Decreasing Colorful Path(并查集,DP)
题意
有一个“无向图”,上面有 N N N个结点和 M M M条边。每一个结点 V V V都有一个对应的权值 A V A_V AV。我们希望从第 1 1 1个结点出发,到达第 N N N个结点,在这个过程中,路上的每个点只经过一次,并且尽可能获得更多的分数。获得分数的规则是这样的:
- 在每一次移动时,如果我们要从结点 U U U移动到结点 V V V,则必须满足 A U ≤ A V A_U\le A_V AU≤AV。
- 如果从 1 1 1号点到 N N N号点的所有路径中,没有任意一条满足这个要求,那么分数即为 0 0 0。
- 如果存在这样的路径,那么我们观察路径上的点的权值,去重以后的个数即为分数。
以题目中的样例1为例,
N
=
5
N=5
N=5时,通过1->3->4->5
到达
N
N
N号点,这四个点的权值去重后共有4个数,因此分数为4。若某条路径路过
4
4
4个点,但对应的权值分别为
10
,
10
,
30
,
40
10,10,30,40
10,10,30,40,则分数应为
3
3
3,因为两个
10
10
10重复了。
此处需要注意的是,这个图可能非常大( 2 ≤ N ≤ 2 × 1 0 5 2\le N \le 2\times10^5 2≤N≤2×105)。
思路
在到达结点 N N N的过程中,路过的点的权值组成的序列,一定是不下降序列,我们的目标是让这个序列去重以后的长度尽可能长,那么我们可以认为,相邻的、权值相同的点可以被合并为同一个点,这个合并的过程可以使用并查集来完成。
在经过这样的压缩以后,整张图就变成了一张有向无环图(DAG)。此时你就可以使用带有备忘录的dfs来解决问题了。我们从 1 1 1号点出发进行深度优先遍历,并从 N N N号结点向前回溯,对于当前结点 c u r cur cur来说,我们用 d p [ c u r ] dp[cur] dp[cur]表示从它出发,到达 N N N号结点所遍历的节点数(包括 c u r cur cur自己)。
为了计算 d p [ c u r ] dp[cur] dp[cur]的值,我们遍历所有他可以到达的结点 v v v, d p [ c u r ] = max { d p [ v ] } + 1 dp[cur]=\max\{dp[v]\}+1 dp[cur]=max{dp[v]}+1, d p [ n ] = 1 dp[n]=1 dp[n]=1。当然, d p [ c u r ] dp[cur] dp[cur]的值只有在大于 0 0 0时才有意义。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10, minn = -1e8;
int n, m, u, v, a[maxn], fa[maxn], dp[maxn], flag;
vector<int> G[maxn];
vector<pair<int, int>> e;
int dfs(int x) {
if (fa[x] == x)return x;
else return fa[x] = dfs(fa[x]);
}
void merge(int x, int y) {
fa[dfs(x)] = dfs(y);
}
void solve(int cur) {
if (dp[cur] != minn)return;
if (cur == dfs(n)) {
//dp[cur]表示从cur出发到n号点,会途径路过多少个点,注意是包括cur的
dp[cur] = 1;
flag = 1; //标记:存在这样的道路
return;
}
for (auto v: G[cur]) {
solve(v);
dp[cur] = max(dp[cur], dp[v] + 1);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
fa[i] = i;
dp[i] = minn;
}
for (int i = 1; i <= m; i++) {
cin >> u >> v;
if (a[u] == a[v])merge(u, v); // 此时u和v可以看成同一个点,进行合并
else e.push_back(make_pair(u, v));
}
for (auto p: e) {
// 必须先处理合并,再去加入其他的边
u = p.first;
v = p.second;
if (a[u] < a[v])G[dfs(u)].push_back(dfs(v));
else G[dfs(v)].push_back(dfs(u));
}
flag = 0;
solve(dfs(1));
if (flag) cout << dp[dfs(1)] << endl;
else cout << 0 << endl;
return 0;
}
F - Hop Sugoroku(DP)
题意
有 n n n个数 A 1 , A 2 , … , A n A_1, A_2, \dots, A_n A1,A2,…,An,对应 n n n个正方形。起初,你把棋子放在 1 1 1号格子的位置,接下来你可以重复以下步骤:
- 棋子可以从第 i i i个位置移动到第 i + A i × x i+A_i\times x i+Ai×x的位置,即:若你在第 i i i个位置,你就可以向后移动任意 A i A_i Ai的 x x x倍。 x x x可以是任意一个正整数,但这个位置不能超过 N N N。
- 你可以在任意一个位置停下来。
现在问你,你最多可以走多少种不同的路线?答案需要对998244353取模。
思路
题目中, A A A数组中所有的元素都是正整数,所以移动的过程方向不变。一个简单的思路是用 d p [ x ] dp[x] dp[x]表示从 x x x出发,向后的路线条数,那么我们可以用伪代码表示:
dp[1] = 1; //初始化为1
for (int i = 1; i <= n; i++) {
for (int j = i + A[i]; j <= n; j += A[i]) {
dp[j] = (dp[j] + dp[i]) % mod; //从i出发可以到达j,因此到达j的方案数应当被更新
}
}
最后, ∑ i = 1 n d p [ i ] \sum\limits_{i=1}^{n}dp[i] i=1∑ndp[i]对mod取模即为最终答案。当 A A A数组中的数很大的时候,这个算法的时间复杂度接近线性级别,但如果数组中的数都不大时,这个算法的时间复杂度就是平方级别,而本题数据量也很大( 1 ≤ N ≤ 2 × 1 0 5 1\le N \le 2\times 10^5 1≤N≤2×105),这样的方法显然是无法通过所有样例的。
一个可能的思路是采用分治,根据A数组中元素的大小分开讨论,若 A [ i ] A[i] A[i]很大,就按照上面的循环直接进行。否则,若 A [ i ] A[i] A[i]比较小,我们利用 d p [ i ] = ∑ i ≡ j ( m o d A j ) d p [ j ] dp[i]=\sum\limits_{i\equiv j\pmod{A_j}}dp[j] dp[i]=i≡j(modAj)∑dp[j]这一性质,维护一个 f f f数组, f [ x ] [ y ] f[x][y] f[x][y]表示:所有对 y y y取模,得到的结果是 x x x的元素都需要标记的值。
那么,怎么区分所谓的“很大”和“比较小”呢?这里我们以 n \sqrt{n} n为分界线,根据数组的大小可以知道,时间复杂度为 O ( n n ) O(n\sqrt{n}) O(nn)。这里的 n \sqrt{n} n可以根据每个输入值灵活改变,也可以根据 n n n可能的最大值,将分界线直接设定为 500 500 500(约等于 2 × 1 0 5 \sqrt{2\times10^5} 2×105)。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10, maxm = 510, mod = 998244353;
int a[maxn], dp[maxn], f[maxm][maxm];
int main() {
int n;
cin >> n;
int m = sqrt(n);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
dp[1] = 1;
int res = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 加上之前累计的标记值
dp[i] = (dp[i] + f[j][i % j]) % mod;
}
//根据当前的结果向后推算
if (a[i] > m) {
//相对比较大,直接推即可
for (int j = i + a[i]; j <= n; j += a[i]) {
dp[j] = (dp[j] + dp[i]) % mod;
}
} else {
f[a[i]][i % a[i]] += dp[i];
f[a[i]][i % a[i]] %= mod;
}
res = (res + dp[i]) % mod; //统计结果
}
cout << res << endl;
return 0;
}
学习交流
以下为学习交流QQ群,群号: 546235402,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。