A. Leftrightarrow(循环)
题意
给出一个字符串,如果该字符串以字符'<'
开始,以'>'
结束,且中间所有字符均为'='
,那么就表示输出Yes
,否则,输出No
。
分析
可以在输入后单独判断首末位的字符,然后对于中间的字符使用循环进行判断。
代码
#include <bits/stdc++.h>
using namespace std;
void solve(){
string s;
cin >> s;
int len = s.size();
if (s[0] == '<' && s[len - 1] == '>') {
for (int i = 1; i < len - 1; i++) {
if (s[i] != '=') {
cout << "No" << endl;
return;
}
}
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
}
int main(){
solve();
return 0;
}
B. Integer Division Returns(思维)
题意
给出一个数字 X ( − 1 0 18 ≤ X ≤ 1 0 18 ) X(-10^{18} \le X \le 10^{18}) X(−1018≤X≤1018),输出 ⌈ X 10 ⌉ \lceil \frac{X}{10} \rceil ⌈10X⌉。
其中, ⌈ a ⌉ \lceil a \rceil ⌈a⌉表示对数字 a a a向上取整。
分析
对于正整数,需要判断除法结果是否有余数,如果存在余数,整除的结果需要加一。
对于负整数,直接进行除法即可。
hint: 本题数据规模较大,需要使用long long
类型存储数据。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){
ll n;
cin >> n;
ll num = n / 10;
if (n > 0 && n % 10 != 0) num++;
cout << num << endl;
}
int main(){
solve();
return 0;
}
C. One Time Swap(前缀和)
题意
给出一个仅包含小写字母的字符串,你需要执行一次以下操作:
- 选择两个不同位置的字符,交换这两个字符
问,经过操作后可以得到多少不同的字符串?
分析
如果交换的字符是相同的,那么交换后就可以得到原串。
如果交换的字符是不同的,那么得到的字符串必然是独立的。
即:
-
1.如果存在相同的字符,必然可以通过交换得到原串(对答案的贡献为1)
-
2.对于每个字符,只要与后面任意一个不同的字符进行交换,得到的均为不同的字符串
那么对于情况1,输入完成后判断是否存在某个字符的出现次数大于1即可。
对于情况2,可以维护一个前缀和,即使用 p r e [ i ] [ j ] pre[i][j] pre[i][j]表示前 i i i个字符中, j j j字符的出现次数。
维护完前缀和后,就可以通过枚举交换的前一个字符,使用前缀和计算该字符后有多少不同的字符,将不同的字符数计入答案中。
枚举结束后,就得到了最后的答案。
坑点:
-
先考虑情况1,遇到情况1就将记录答案的变量设为1。
-
答案数量很大,需要使用
long long
类型进行存储。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
string s;
int sum[1000005][30];
void solve(){
cin >> s;
int len = s.size();
s = "#" + s;
int flag = 0;
for (int i = 1; i <= len; i++) {
for (int j = 0; j < 26; j++) {
sum[i][j] = sum[i - 1][j];
}
sum[i][s[i] - 'a']++;
if (sum[i][s[i] - 'a'] != 1) {
flag = 1;
}
}
ll ans = flag;
for (int i = 1; i <= len; i++) {
for (int j = 0; j < 26; j++) {
if (j != s[i] - 'a') {
ans += sum[len][j] - sum[i][j];
}
}
}
cout << ans << endl;
}
int main(){
solve();
return 0;
}
D.Tiling(DFS)
题意
给出 n n n个瓷砖,问能否将 h × w h \times w h×w的网格铺满,其中每个瓷砖的大小不同,且每个瓷砖可以进行旋转(即宽高互换),但每个瓷砖只能使用一次。
分析
由于题目数据规模很小 ( n ≤ 7 , h ≤ 10 , w ≤ 10 ) (n \le 7, h \le 10, w \le 10) (n≤7,h≤10,w≤10),因此可以直接进行搜索,对于每个位置检查能否放入瓷砖,如果能就将瓷砖放入。
如果所有网格均被放入瓷砖,那么就表示存在方案,如果搜索结束还没找到方案,那么就表示不存在方案。
hint: 如果瓷砖长和宽不同,需要在每次搜索时旋转一下,两种状态都要尝试装入。
代码
#include<bits/stdc++.h>
using namespace std;
int n, h, w, flag, v[15], vis[15][15], a[15], b[15];
bool check(int x, int y, int p) {
if (x + a[p] - 1 > h || y + b[p] - 1 > w) return 0;
for (int i = x; i < x + a[p]; i++) {
for (int j = y; j < y + b[p]; j++) {
if (vis[i][j] == 1) return 0;
}
}
return 1;
}
void color(int x, int y, int p, int c) {
for (int i = x; i < x + a[p]; i++) {
for (int j = y; j < y + b[p]; j++) {
vis[i][j] = c;
}
}
}
void dfs(int x, int y) {
if (flag || x > h) {
flag = 1;
return;
}
if (vis[x][y]) {
dfs(x, y + 1);
return;
}
if (y > w) {
dfs(x + 1, 1);
return;
}
for (int i = 1; i <= n; i++) {
if (v[i]) continue;
if (check(x, y, i)) {
color(x, y, i, 1);
v[i] = 1;
dfs(x, y + 1);
v[i] = 0;
color(x, y, i, 0);
}
if (a[i] != b[i]) {
swap(a[i], b[i]);
if (check(x, y, i)) {
color(x, y, i, 1);
v[i] = 1;
dfs(x, y + 1);
v[i] = 0;
color(x, y, i, 0);
}
}
}
}
int main() {
cin >> n >> h >> w;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
dfs(1, 1);
if (flag) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}
E. Colorful Subsequence(DP)
题意
给定序列,删除恰好K个元素,使得相邻不同色,且剩余价值总和最大。
前置分析
毋庸置疑的一个动态规划问题。简单版本的基础状态定义可以是:
DP[i][j]表示:前i个元素中,删除j个元素剩余的最大价值。
当然这个状态是无法向下推导的,因为无法保证相邻不同色的约定。因此要增加状态
DP[i][j][k]表示:前i个元素中,删除j个元素,最后结尾为k颜色的最大价值。
那么考虑的决策点就是当前元素删/不删。
删:DP[i][j][k] = max{ DP[i-1][j-1][任意k] }
不删:DP[i][j][k] = max{ DP[i-1][j][非k以外的其他颜色] } + V[i]
这是一个 O ( N 3 K ) O(N^3K) O(N3K) 的复杂度,或者我们也可以利用维护前后缀最值的方式来优化到 O ( N 2 K ) O(N^2K) O(N2K)。
但是对于本题的复杂度要求是无法通过的。本题的期望复杂度是 O ( N K ) O(NK) O(NK)
优化
本题的优化思路其实并不难想到,并且也非常常规。
首先,上面的基础DP思路是可行的,仅仅是效率欠佳。
其次,对于当前的元素i,可以与其相邻的颜色是除了i以外的所有颜色,如果我们每一种颜色都考虑一下,纵然可以保证正确,但是效率较低。
换一个角度来看,如果我们从最优解的角度来选择,当我们保留当前元素i的时候,我们优先看DP[i-1][j] 的最优解,而忽略其颜色。简单直观来讲此时的转移方程就会是 D P [ i ] [ j ] = D P [ i − 1 ] [ j ] + V [ i ] DP[i][j] = DP[i-1][j] + V[i] DP[i][j]=DP[i−1][j]+V[i], 当然这是错误的,因为没有考虑颜色是否相同。
接下来考虑颜色,如果DP[i-1][j] 的最优解颜色与i元素同色,我们这样转移就是错误的,那么我们可以看DP[i-1][j]的次优解,如果次优解不同色,是不是我们就可以用次优解来转移。当次优解与最优解不同色的时候,也就与i不同色,当然是可以的。如果次优解与最优解也是同色,那也是不可以的,我们就要找第三优、第四优。。。
但是从另外一个角度来看,如果最优解与次优解的颜色相同,那么次优解就没有被记录的意义了,因为选不了最优解也同样因为颜色冲突而选不了次优解。
综上我们就得出了解决问题的方案:
我们只需要记录DP[i][j] 的最优解与次优解,与其对应的颜色。转移的时候就可以O(1)转移。
代码
#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for(int i = 0; i < n; ++i)
#define MAX_K 500
#define INF (long long)2e+18
int main(){
int n,k;
long long c,v;
long long dp[MAX_K+1][2][2];
cin>>n>>k;
rep(j,k+1)rep(ii,2){dp[j][ii][0]=-1;dp[j][ii][1]=-INF;}
dp[0][0][0]=0,dp[0][0][1]=0;
rep(i,n){
cin>>c>>v;
for(int j=k;j>=1;j--){
if(dp[j][0][0]!=c){
dp[j][0][0]=c;
dp[j][0][1]+=v;
}
else{
dp[j][0][0]=c;
dp[j][0][1]=dp[j][1][1]+v;
}
dp[j][1][0]=-1;
dp[j][1][1]=-INF;
rep(kk,2){
if(dp[j-1][kk][1]>=dp[j][0][1]){
if(dp[j-1][kk][0]!=dp[j][0][0]){
dp[j][1][0]=dp[j][0][0];
dp[j][1][1]=dp[j][0][1];
}
dp[j][0][0]=dp[j-1][kk][0];
dp[j][0][1]=dp[j-1][kk][1];
}
else if((dp[j-1][kk][1]>=dp[j][1][1])&&(dp[j-1][kk][0]!=dp[j][0][0])){
dp[j][1][0]=dp[j-1][kk][0];
dp[j][1][1]=dp[j-1][kk][1];
}
}
}
if(dp[0][0][0]!=c){
dp[0][0][0]=c;
dp[0][0][1]+=v;
}
else{
dp[0][0][1]=-INF;
}
}
if(dp[k][0][1]<0)cout<<"-1"<<endl;
else cout<<dp[k][0][1]<<endl;
return 0;
}
F.Many Lamps(DFS)
题意
有一个包含 N N N个点, M M M条边的简单图。
在图上每个点上都有一盏灯,在开始时,所有灯都是关着的。
问:是否有可能在执行 0 ∼ M 0 \sim M 0∼M次以下操作后,使得恰好 K K K盏灯是亮着的:
- 选择一条边,让这条边连接的两个点上的灯切换状态。
如果可以使得恰好 K K K盏灯被打开,打印你的操作序列。
分析
不难想到,只要 K K K为奇数时,必然无解。
证明:每次操作只存在以下几种情况:
-
两盏灯都在关闭状态,打开这两盏灯,灯的总数增加2.
-
两盏灯都在开启状态,关闭这两盏灯,灯的总数减少2.
-
两盏灯的状态为一开一关,操作后灯的状态依然为一开一关,灯的总数不变。
因此,开启的灯的数量为偶数时,才有可能存在解。
那么该怎么做呢?
可以把给出的图视为一棵树,我们从叶节点开始向上操作,对于所有叶节点(假设有 a a a个),由于开始时均未被点亮,因此,选择这些叶节点与其父结点之间连接的边进行操作,结束 a a a次操作后,必然所有叶节点均被点亮,但此时叶节点的父结点的状态则还未确定,然后可以将叶节点均视为已被删除,接下来对这些结点的父结点进行操作,依此类推。那么,对于一个包含 x x x个节点的子树,经过 x − 1 x - 1 x−1次操作后,必然可以得到一个 x − 1 x - 1 x−1或 x x x个被点亮的灯(当 x x x为奇数时,无法点亮根节点,否则,整棵树都可以被点亮)。
因此,对于给出的所有图视为树进行搜索(本题没有保证图是连通的),先递归走到底,返回后检查走向的点是否被点亮,如果没被点亮,且当前被点亮的灯的总数仍然小于给出的 K K K,就操作一下这条边,使走向的点上的灯的状态为打开状态。
结束遍历后,如果被点亮的灯的数量等于
K
K
K,输出搜索过程中操作的边,否则,输出No
。
代码:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int N, M, K;
cin >> N >> M >> K;
vector<vector<pair<int, int>>> G(N);
for (int i = 1; i <= M; i++) {
int u, v;
cin >> u >> v;
--u, --v;
G[u].emplace_back(v, i);
G[v].emplace_back(u, i);
}
int Y = 0;
vector<int> ans, vis(N), lamp(N);
for (int i = 0; i < N; i++) {
auto dfs = [&](auto rc, int c) -> void {
vis[c] = 1;
for (auto& [d, id] : G[c]) {
if (vis[d]) continue;
rc(rc, d);
if (lamp[d] == 0 and Y < K) {
Y -= lamp[d] + lamp[c];
lamp[d] ^= 1, lamp[c] ^= 1;
Y += lamp[d] + lamp[c];
ans.push_back(id);
}
}
};
if (!vis[i]) dfs(dfs, i);
}
if (K != Y) {
cout << "No" << endl;
} else {
cout << "Yes" << endl;
cout << ans.size() << endl;
for (int i = 0; i < (int)ans.size(); i++) cout << (i ? " " : "") << ans[i];
cout << endl;
}
}
赛后交流
在比赛结束后,会在交流群中给出比赛题解,同学们可以在赛后查看题解进行补题。
群号: 704572101,赛后大家可以一起交流做题思路,分享做题技巧,欢迎大家的加入。