A.Problemsolving Log(计数)
题意:
有 26 26 26个问题 A ∼ Z A \sim Z A∼Z,分别需要尝试 1 ∼ 26 1 \sim 26 1∼26次才能通过。
给出一个字符串,里面包含的每个字母代表着这道题目的一次尝试,问:总共通过了多少题目。
分析:
使用数组记录每个字母的出现次数,如果 A A A出现了一次, B B B出现了两次,…,就代表该题目通过了,记录过题数量即可。
代码:
#include <bits/stdc++.h>using namespace std;
const int N = 100005;int vis[30];void solve() {memset(vis, 0, sizeof (vis));int n;string s;cin >> n >> s;for (int i = 0; i < n; i++) {vis[s[i] - 'A']++;}int ans = 0;for (int i = 0; i <= 25; i++) {if (vis[i] > i) ans++;}cout << ans << endl;
}int main() {int Case;cin >> Case;while (Case--) {solve();}return 0;
}
B.Preparing for the Contest(构造)
题意:
给出一个难度序列,序列中 a [ i ] > a [ i − 1 ] a[i] > a[i - 1] a[i]>a[i−1]的个数为该序列的难度系数,求长度为 n n n且难度系数为 k k k的序列。
分析:
首先,让序列为增序,即 1 , 2 , . . . , n 1, 2, ..., n 1,2,...,n,然后,除最后 k k k个元素外,将前面部分翻转,变为: n − k , n − k − 1 , . . . , 1 , n − k + 1 , . . . , n n - k, n - k - 1, ..., 1, n - k + 1, ..., n n−k,n−k−1,...,1,n−k+1,...,n。
此时,前面的 n − k ∼ 1 n- k \sim 1 n−k∼1均为降序,不会产生难度系数,后面的 k + 1 k + 1 k+1个元素(包含前面部分结尾的1)为升序,会产生 k k k个难度系数,满足题目要求。
代码:
#include <bits/stdc++.h>using namespace std;
const int N = 100005;void solve() {int n, k;cin >> n >> k;for (int i = n - k; i >= 1; i--) {cout << i << ' ';}for (int i = n - k + 1; i <= n; i++) {cout << i << ' ';}cout << endl;
}int main() {int Case;cin >> Case;while (Case--) {solve();}return 0;
}
C.Quests(枚举)
题意:
有 n n n个关卡,你只能从前往后进行闯关(通过上一关才能进入下一关),第一次通过第 i i i个关卡时可以获得 a i a_i ai点积分,之后再次通过第 i i i个关卡时可以获得 b i b_i bi点积分,问如果能闯关 k k k次,最多能获得多少积分。
分析:
不难发现,最多只会在一个关卡进行多次通过,那么只需要枚举最后到达的关卡,并记录过程中遇到过的关卡的 b i b_i bi中的最大值 m a x b i max_{b_{i}} maxbi,那么到达第 i i i关的最大积分为: a 1 + a 2 + . . . + a i + ( k − i ) × m a x b i , i ≤ k a_1 + a_2 + ... + a_i + (k - i) \times max_{b_{i}},i \le k a1+a2+...+ai+(k−i)×maxbi,i≤k。
代码:
#include <bits/stdc++.h>using namespace std;
const int N = 2e5 + 5e2;int a[N], b[N];void solve() {int n, k;cin >> n >> k;for (int i = 1; i <= n; i++) {cin >> a[i];}for (int i = 1; i <= n; i++) {cin >> b[i];}int ans = 0, tmp = 0, maxn = 0;for (int i = 1; i <= n; i++) {tmp += a[i];maxn = max(maxn, b[i]);if (k >= i) {ans = max(ans, tmp + max(0, k - i) * maxn);}}cout << ans << endl;
}int main() {int Case;cin >> Case;while (Case--) {solve();}return 0;
}
D.Three Activities(枚举)
题意:
在 n n n天里,每天有三种活动:
-
滑雪
-
看电影
-
玩游戏
在第 i i i天,有 a i a_i ai人会参加活动 1 1 1,有 b i b_i bi人会参加活动 2 2 2,有 c i c_i ci人会参加活动 3 3 3。
每个活动只能进行一次,且每天只能完成一个活动,问最多能召集多少个朋友完成所有活动。
分析:
不难想到,每个活动只有人数最多的三天会被选择,仅记录每个活动人数最多的三天,枚举所有的方案,记录可行的方案中人数最大的即可。
代码:
#include <bits/stdc++.h>using namespace std;
const int N = 2e5 + 5e2;struct Node{int val, id;bool operator < (const Node &o) const {return val > o.val;}
};Node a[N], b[N], c[N];void solve() {int n;cin >> n;for (int i = 1; i <= n; i++) {cin >> a[i].val;a[i].id = i;}for (int i = 1; i <= n; i++) {cin >> b[i].val;b[i].id = i;}for (int i = 1; i <= n; i++) {cin >> c[i].val;c[i].id = i;}sort(a + 1, a + n + 1);sort(b + 1, b + n + 1);sort(c + 1, c + n + 1);int ans = 0;for (int i = 1; i <= 3; i++) {for (int j = 1; j <= 3; j++) {for (int k = 1; k <= 3; k++) {if (a[i].id == b[j].id || b[j].id == c[k].id || a[i].id == c[k].id) continue;ans = max(ans, a[i].val + b[j].val + c[k].val);}}}cout << ans << endl;
}int main() {int Case;cin >> Case;while (Case--) {solve();}return 0;
}
E.Game with Marbles(贪心)
题意:
两个人进行石子游戏,每个人均拥有 n n n种不同的石子,且每种石子的数量不同(但两人的石子种类相同),当轮到某人操作时,他可以选择手上的一种石子,丢掉其中一颗石子,并让对手丢掉相同颜色的全部石子(只有对手手上还有这种颜色的石子时才能进行)。
游戏结束时,先手手上剩下的石子数量减去后手手上剩余的石子数量就是最后的得分。
先手希望结束时得分尽可能高,后手希望得分尽可能低,而两人都极为聪明,每次操作均会选择最优策略,问最后的得分是多少?
分析:
对于Easy Version
,由于石子数量较小 ( n ≤ 6 ) (n \le 6) (n≤6),那么可以对两人的操作进行模拟,来得到答案。
对于Hard Version
,考虑贪心,通过模拟后可以发现,实际上每个人操作时,会选择剩下的石子中 a i + b i a_i + b_i ai+bi最大的颜色 i i i进行操作,因为哪怕自己这种颜色的石子很少,但也可以消除对手最多的石子。
因此,使用结构体存储先后手的石子数量,对两者拥有的石子数量总和从大到小进行排序,然后从前往后模拟操作即可。
代码:
#include <bits/stdc++.h>using namespace std;
typedef long long LL;
const int N = 2e5 + 5e2;struct Node{int a, b;bool operator < (const Node &o) const {return a + b > o.a + o.b;}
};Node g[N];void solve() {int n;cin >> n;for (int i = 1; i <= n; i++) {cin >> g[i].a;}for (int i = 1; i <= n; i++) {cin >> g[i].b;}sort(g + 1, g + n + 1);LL ans = 0;for (int i = 1; i <= n; i++) {if (i & 1) {ans += g[i].a - 1;} else {ans -= g[i].b - 1;}}cout << ans << endl;
}int main() {int Case;cin >> Case;while (Case--) {solve();}return 0;
}
F.Programming Competition(树形DP)
题意:
有一个公司需要选择若干个人两两组队参与比赛,且选择的两个人中不能存在上下级关系(直接和间接均不行),问:最多可以组成多少只队伍?
分析:
实际上公司的上下级关系就是一个树形的关系,对于上下级关系,只要两个节点不在同一棵子树上,那么可以任意进行组队。
因此,当某个节点的子树数量大于 1 1 1时,使用 s u m sum sum表示所有子树节点之和, m a x n maxn maxn表示最大子树中节点数量, m a x c maxc maxc表示最大子树中已经匹配的队伍数,有以下两种情况:
-
最大的子树中节点数量 m a x n maxn maxn减去以及可以完成匹配的人数后小于等于其他子树中的节点数量总和 s u m − m a x n sum - maxn sum−maxn,即 m a x n − m a x c × 2 ≤ s u m − m a x n maxn - maxc \times 2 \le sum - maxn maxn−maxc×2≤sum−maxn,此时能组成的队伍数为 ⌊ s u m 2 ⌋ \lfloor \frac{sum}{2} \rfloor ⌊2sum⌋
-
否则,此时能组成的队伍数为已经匹配的人数加上其他子树中所有节点数量,即 m a x c + s u m − m a x n maxc + sum - maxn maxc+sum−maxn。
如果子树数量等于 1 1 1,那么只能直接继承子树的状态。
代码:
#include <bits/stdc++.h>using namespace std;
typedef long long LL;
const int N = 2e5 + 5e2;vector<int> G[N];
int n, sz[N], dp[N];//节点总数,子树大小,子树能组成的队伍数void dfs(int root) {sz[root] = 1;dp[root] = 0;int len = G[root].size();int maxn = 0, maxc = 0, sum = 0;for (int i = 0; i < len; i++) {int v = G[root][i];dfs(v);sz[root] += sz[v];dp[root] = max(dp[root], dp[v]);if (maxn < sz[v]) {maxn = sz[v];maxc = dp[v];}sum += sz[v];}if (len <= 1) return;if (sum - maxn >= maxn - maxc * 2) {dp[root] = sum / 2;} else {dp[root] = sum - maxn + maxc;}
}void solve() {cin >> n;for (int i = 1; i <= n; i++) {G[i].clear();}for (int i = 2; i <= n; i++) {int u;cin >> u;G[u].push_back(i);}dfs(1);cout << dp[1] << endl;
}int main() {int Case;cin >> Case;while (Case--) {solve();}return 0;
}
G.Light Bulbs(线段树优化建图+强连通分量)
题意:
本题两个版本不同之处在于数据范围。
有 2 n 2n 2n个灯泡排成一排。每个灯泡都有从 1 1 1到 n n n的颜色(每种颜色正好有两个灯泡)。
最初,所有的灯泡都是关掉的。选择一组灯泡 S S S,按任意顺序执行任意次数以下操作:
- 选择两个颜色相同的灯泡 i i i和 j j j,且其中一个是亮着的,打开另一个灯泡;
- 选择三个灯泡 i , j , k i,j,k i,j,k,要求灯泡 i i i和 k k k都是亮着且颜色相同,灯泡 j j j在它们之间(即 i < j < k i < j < k i<j<k),打开灯泡 j j j。
题目希望选择一组灯泡 S S S,通过执行上述操作,确保所有灯泡都是打开的。
计算两个数字:
- 最初可以选择的最小灯泡数量,即一组灯泡 S S S包含的最小数目。
- 最小灯泡数量对应的的方案数(结果对998244353取模)。
分析:
对于简单版本,假设最坏情况下,整个区间都变成一个环。例如 [ 1 , 2 , 1 , 2 ] [1,2,1,2] [1,2,1,2],点 1 1 1可以到点 2 2 2,点 2 2 2可以到点 1 1 1,构成环。如果是 [ 1 , 2 , 2 , 1 ] [1,2,2,1] [1,2,2,1],点 1 1 1可以到点 2 2 2,但是点 2 2 2不能到 1 1 1。把环缩成点后就变成了经典的拓扑排序问题。最小数目就是这个拓扑排序入度为 0 0 0的点,方案数就是入度为 0 0 0的点相乘即可。
困难版本的思路如下:
首先可以发现同种颜色的点我们只会取一个,然后发现如果我们取了某种对应位置为 [ l , r ] [l,r] [l,r]的点,那么我们同时也取了 [ l + 1 , r − 1 ] [l+1,r−1] [l+1,r−1]之间的所有点。
如果取了颜色 a a a后,颜色 b b b也能自动被取,连一条从 a a a指向 b b b的边。
对这样的图跑强连通分量,只需要操作所有入度为 0 0 0的点即可,假设一个入度为 0 0 0的强连通分量里有 x x x种颜色,那么有 2 x 2x 2x种取法,用乘法原理统计答案即可。
直接暴力连边复杂度 O ( n 2 ) O(n^2) O(n2),这样只能做 e a s y easy easy版本。
对于 h a r d hard hard版本,使用线段树优化建图,用线段树实现对一个区间内的所有点连边,然后再跑强连通分量即可。但是此时需要操作就不一定是入度为 0 0 0的强连通分量了,因为有可能入度为 0 0 0的是线段树上的点组成的强连通分量。
此时可以按照拓扑序跑一遍 D P DP DP,找到所有没有任何颜色进入(线段树上的点可以进入)的强连通分量再统计答案即可。时间复杂度为 O ( n log n ) O(n \log n) O(nlogn),空间复杂度为 O ( n ) O(n) O(n)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN=4e5+5;
const int MOD=998244353;struct S1{vector<vector<int>>g,scc;vector<int>dfn,low,stk,id;vector<bool>ins;int ts,n;S1(const vector<vector<int> > &g) : g(g){n = (int)g.size();dfn.assign(n, 0);low.assign(n, 0);id.assign(n, -1);ins.assign(n, false);stk.reserve(n);ts = 0;build();}void tarjan(int u){dfn[u] = low[u] = ++ts;stk.push_back(u);ins[u] = 1;for(auto j : g[u]){if (!dfn[j]){tarjan(j);low[u] = min(low[u], low[j]);}else if (ins[j]) low[u] = min(low[u], dfn[j]);}if (dfn[u] == low[u]){int scc_cnt = scc.size();scc.push_back({});int y;do{y = stk.back();stk.pop_back();id[y] = scc_cnt;ins[y] = 0;scc.back().push_back(y);}while(y != u);}}void build(){for(int i = 0; i < n; i++){if (!dfn[i]){tarjan(i);}}}
};vector<vector<int> > g;
int a[MAXN];
int n;void build(int u, int l, int r){if (l == r){g[u].push_back(8 * n + a[r]);return;}int mid = (l + r) / 2;g[u].push_back(2 * u);g[u].push_back(2 * u + 1);build(2 * u, l, mid); build(2 * u + 1, mid + 1, r);
}void modify(int u, int l, int r, int L, int R, int x){if (l > R || r < L) return;if (l >= L && r <= R){g[x].push_back(u);return;}int mid = (l + r) / 2;modify(2 * u, l, mid, L, R, x);modify(2 * u + 1, mid + 1, r, L, R, x);
}int main(){int T;cin >>T;while(T--){cin >> n;vector<array<int, 2> > pos(n + 1);for(int i = 1; i <= 2 * n; i++){cin >> a[i];if (pos[a[i]][0] == 0){pos[a[i]][0] = i;}else{pos[a[i]][1] = i;}}g.assign(9 * n + 1, {});build(1, 1, 2 * n);for(int i = 1; i <= n; i++){auto [l, r] = pos[i];if (l + 1 <= r - 1){modify(1, 1, 2 * n, l + 1, r - 1, 8 * n + i);}}S1 s1(g);const int m = s1.scc.size();vector<int> c(m);for(int i = 0; i < m; i++){int s = 0;for(auto x : s1.scc[i]){s += (x > 8 * n);}c[i] = s;}int cnt = 0,sum = 1;vector<int>bad(m);for(int i = m - 1; i >= 0; i--){if (!bad[i] && c[i] > 0){cnt += 1;sum = 2LL * sum * c[i] % MOD;}bad[i] |= (c[i] > 0);for(auto x : s1.scc[i]){for(auto j : g[x]){if (s1.id[j] != i){bad[s1.id[j]] |= bad[i];}}}}cout<<cnt<<' '<<sum<<endl;}return 0;
}
学习交流
以下为学习交流QQ群,群号: 546235402,每周题解完成后都会转发到群中,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。