AtCoder Beginner Contest 345 A~F

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(1018X1018),输出 ⌈ 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) (n7,h10,w10),因此可以直接进行搜索,对于每个位置检查能否放入瓷砖,如果能就将瓷砖放入。

如果所有网格均被放入瓷砖,那么就表示存在方案,如果搜索结束还没找到方案,那么就表示不存在方案。

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[i1][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 0M次以下操作后,使得恰好 K K K盏灯是亮着的:

  • 选择一条边,让这条边连接的两个点上的灯切换状态。

如果可以使得恰好 K K K盏灯被打开,打印你的操作序列。

分析

不难想到,只要 K K K为奇数时,必然无解。

证明:每次操作只存在以下几种情况:

  1. 两盏灯都在关闭状态,打开这两盏灯,灯的总数增加2.

  2. 两盏灯都在开启状态,关闭这两盏灯,灯的总数减少2.

  3. 两盏灯的状态为一开一关,操作后灯的状态依然为一开一关,灯的总数不变。

因此,开启的灯的数量为偶数时,才有可能存在解。

那么该怎么做呢?

可以把给出的图视为一棵树,我们从叶节点开始向上操作,对于所有叶节点(假设有 a a a个),由于开始时均未被点亮,因此,选择这些叶节点与其父结点之间连接的边进行操作,结束 a a a次操作后,必然所有叶节点均被点亮,但此时叶节点的父结点的状态则还未确定,然后可以将叶节点均视为已被删除,接下来对这些结点的父结点进行操作,依此类推。那么,对于一个包含 x x x个节点的子树,经过 x − 1 x - 1 x1次操作后,必然可以得到一个 x − 1 x - 1 x1 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,赛后大家可以一起交流做题思路,分享做题技巧,欢迎大家的加入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值