Codeforces Round #658 (Div. 2)详解

Codeforces Round #658 (Div. 2)

题目链接

A. Common Subsequence

题意:

输入两个整数数组a,b,找到两个数组的最短公共子序列c。子序列的概念:如果ca的子序列,则满足a通过删除若干(也可以是0)个元素可以得到c

数据范围:

length(a), length(b) <= 1000; case <= 1000, 1 <= a[i], b[i] <= 1000

思路:

既然是找到最短的公共子序列,那么只要找到在一个a, b中都出现过的元素即可。这里我们先读入数组a,并使用无序堆unordered_set<int> s来存储,然后再查找b中的元素是否能在s中找到。
[unordered_set定义在<unordered_set>头文件中,查找操作时间复杂度是 O ( 1 ) O(1) O(1),而set是有序堆,查找操作时间复杂度是 O ( l o g n ) O(logn) O(logn),类似的还有unordered_map, map]

代码:
int main(int argc, char * argv[]) 
{
	int T;
	cin >> T;
	while (T--) {
		int n, m;
		cin >> n >> m;
		unordered_set<int> s;
		int a, b, v = 0;
		for (int i = 0; i < n; ++i) {
			cin >> a;
			s.insert(a);
		}
		for (int i = 0; i < m; ++i) {
			cin >> b;
			if (s.count(b)) v = b;
		}
 
		if (v) cout << "YES\n" << 1 << " " << v << endl;
		else cout << "NO\n";
 
	}
    return 0;
}

B. Sequential Nim

题意:

n堆石头,第i堆有a[i]个石头,两个人玩游戏,每个人轮流从第一个非空石头堆中拿石头,最后没有石子可以拿的人输掉游戏,如果每个人都采取最优的决策,请判断先手的人是否能赢得游戏。

数据范围:

n <= 1e5; case <= 1000, 1 <= a[i] <= 1e9

思路:

先假设A先手,B后手。首先如果只有一堆石子,那么A赢。如果有两堆石子,且a[0] > 1,那么总是A赢得游戏,因为先手的人总可以先拿走a[0] - 1个石子,只留下一个石子,然后B拿走剩余的一个石子,然后A拿走最后一堆所有的石子,B没有石子可拿,A赢下游戏。那么我们就可以从后往前推,判断从当前这堆石子出发,A是否可以赢得游戏。由于只有一堆石子的情况下A可以赢下游戏,所以要从倒数第二堆石子开始递推。当f=true时表示A赢下游戏,否则B赢下游戏。分两种情况讨论:

  • a[i] == 1时,那么在剩下的游戏中,A就变成了后手,B变成了先手,A,B胜负互换,所以这里f = !f
  • a[i] > 1那么A总是可以赢下游戏的,因为如果想要在接下来的游戏中交换先后手那么可以直接拿光这堆石头,如果不想交换先后手,则拿a[i] - 1个石头。
代码:
const int N = 1e5 + 5;
int a[N];
 
int main(int argc, char * argv[]) 
{ 
	int T;
	cin >> T;
	while (T--) {
		int n;
		cin >> n;
 
		for (int i = 0; i < n; ++i) cin >> a[i];
		bool f = true;
		for (int i = n - 2; i >= 0; --i) {
			if (a[i] == 1) f = !f;
			else {
				if (!f) f = true; 
			}
		}
 
		if (f) cout << "First\n";
		else cout << "Second\n";
	}
 
    return 0;
}

C1. Prefix Flip (Easy Version)

题意:

给两个二进制字符串(只包含字符0和1)a, b长度为n,现在可以对字符串进行如下操作:对字符串选择任意长度的前缀,先进行字符反转(0变成1,1变成0),然后再将该前缀子字符串进行翻转,例如:s=001011,当选取前缀长度为3时,先反转字符得到110011,然后翻转得到011011。现在任务是将字符串通过这种操作a变成b,最多只有3n步,输出总共的步数以及每一步操作的前缀长度。

数据范围:

n <= 1000; case <= 1000

思路:

可以枚举a[i], 0 <= i < n变为b[i],但是需要保证每一次操作不会影响到之前已经转变成功的字符,由于总的步数需要限制在3n所以转变一个字符需要在3步内,怎么保证在转变a[i]的时候a[0],...a[i - 1]都不发生变化呢?这里提供一个简单的思路,就是当a[i] != b[i],先对前缀长度为i的字符串操作,然和对前缀长度为1的字符串操作,然后再对前缀长度为i的字符串操作,这里可以看出,字符a[i]反转了3次,而a[0] - a[i - 1]反转了两次,相当于不变。

代码:
int main(int argc, char * argv[]) 
{
 	int T;
 	cin >> T;
 	string a, b;
 	vector<int> ops;
 	while (T--) {
 		int n;
 		cin >> n;
 		cin >> a >> b;
 		ops.clear();
        for (int i = 0; i < n; ++i) {
        	if (a[i] != b[i]) {
        		ops.push_back(i + 1);
        		ops.push_back(1);
        		ops.push_back(i + 1);
        	}
        }
        cout << ops.size();
        for (auto & v : ops) cout << " " << v;
        cout << endl;
 	}
    return 0;
}

C2. Prefix Flip (Hard Version)

题意:

给两个二进制字符串(只包含字符0和1)a, b长度为n,现在可以对字符串进行如下操作:对于字符串选择任意长度的前缀,先进行字符反转(0变成1,1变成0),然后再将该前缀子字符串进行翻转,例如:s=001011,当选取前缀长度为3时,先反转字符得到110011,然后翻转得到011011。现在任务是将字符串通过这种操作a变成b,最多只有2n步,输出总共的步数以及每一步操作的前缀长度。

数据范围:

n <= 1e5; case <= 1000

思路:

C1的困难版,这里官方Tutorial给出了两种方法。这里介绍一下较为巧妙的思路。先把字符串a变为全0的字符串,再把b变为全0的字符串,同时把关于b的操作翻转接到a的操作上,就可以实现将a变为b。那如何实现在n步内将a变为全0的字符串呢?只需要逐步将前缀变为全1或者全0的字符串即可,例如:0101->1101->0001->1111->0000
[这里为了保证a, b转化为全0字符串,在a, b后面添加了一个字符0,简化代码;rbegin(), rend()可以实现对容器进行逆序遍历]

代码:
int main(int argc, char * argv[]) 
{
	int T;
	cin >> T;
	string a, b;
	int n;
	while (T--) {
		cin >> n;
		cin >> a >> b;
		a.push_back('0');
		b.push_back('0');
		vector<int> aops, bops;
		for (int k = 1; k <= n; ++k) {
			if (a[k] != a[k - 1]) aops.push_back(k);
			if (b[k] != b[k - 1]) bops.push_back(k);
		}
		int len = aops.size() + bops.size();
		cout << len;
		for (auto it = aops.begin(); it != aops.end(); ++it) cout << " " << *it;
		for (auto it = bops.rbegin(); it != bops.rend(); ++it) cout << " " << *it;
		cout << endl;
	}
 
 
    return 0;
}

D. Unmerge

题意:

先解释一下归并的操作对于序列a, b
m e r g e ( a , b ) = [ a 1 ] + m e r g e ( [ a 2 , … , a n ] , b ) , a 1 < b 1 merge(a,b)=[a_1]+merge([a_2,…,a_n],b), a_1 < b_1 merge(a,b)=[a1]+merge([a2,,an],b),a1<b1
m e r g e ( a , b ) = [ b 1 ] + m e r g e ( [ b 2 , … , b m ] , a ) , a 1 > b 1 merge(a,b)=[b_1]+merge([b_2,…,b_m],a), a_1 > b_1 merge(a,b)=[b1]+merge([b2,,bm],a),a1>b1
这个操作是个迭代操作直至a或者b为空,即合成一个数组。
给一个长度为2n置换数组,判断该数组是否可以由两个长度为n的数组归并合成。
置换数组的概念:对于长度为n的数组,数组元素只包含1, 2, ...n,顺序可以是任意的,比如[4, 2, 3, 1, 5]就是置换数组,但是[1, 2, 2]不是。

数据范围:

n <= 2000; case <= 1000

思路:

熟悉归并排序的同学都很清楚,这就是归并排序的逆过程,这里给出归并排序的代码。

void merge(vector<int>& arr, int L, int R) {
    if (L == R) return 0;
    int M = (L + R) >> 1;
    merge(arr, L, M);
    merge(arr, M + 1, R);
    
    vector<int> list;
    int Lind = L, Rind = M + 1;
    while (Lind <= M && Rind <= R) {
        if (arr[Lind] <= arr[Rind]) list.push_back(arr[Lind++]);
        else list.push_back(arr[Rind++]);
    }
    
    while (Lind <= M) list.push_back(arr[Lind++]);
    while (Rind <= R) list.push_back(arr[Rind++]);

    for (int k = L; k <= M; ++k) arr[k] = list[k - L];
}

可以看到后面两个循环是为了保证左右子数组都遍历完,那么后面两个循环有且只有一个起作用,这保证该循环添加到新数组list后面的一段元素必定是某一个子数组的后缀,要么是左子数组的,要么是右子数组的。这个后缀有个明显的特征就是第一个元素是最大的,然后我们在list中删除这个后缀。得到一个新数组,然后问题又变成了原来的问题,新数组是否可以通过两个子数组merge得到,又可以进行上面的操作,如此迭代下去,就可以得到一系列的子数组。例如:[2, 3, 1, 4]->{[2, 3, 1], [4]}->{[2], [3, 1], [4]}.
这样问题就转化为这些子数组是否可以组成两个长度为n的数组,因为总长度是2*n,所以可以转化为是否可以找到若干子数组组成一个长度为n的数组,所以这就转化为了0-1背包问题,最简单的可以写成 O ( n 2 ) O(n^2) O(n2),这也完全可以AC,这里提供一个官方的 O ( n n ) O(n\sqrt n) O(nn )写法(为啥是 O ( n n ) O(n\sqrt n) O(nn )我也不知道😓)。

代码:
const int N = 4e3 + 5;
int dp[N];
bool vis[N];
int main(int argc, char * argv[]) 
{
	int T, n;
	cin >> T;
	while (T--) {
		memset(vis, false, sizeof(vis)); vis[0] = true;
 
		cin >> n;
		vector<int> ind;
		int maxE = 0, pk;
		for (int k = 0; k < 2*n; ++k) {
			cin >> pk;
			if (pk > maxE) {
				maxE = pk;
				ind.push_back(k);
			}
		}
		ind.push_back(2*n);
		vector<int> blens; // lengths of subarray
		int size_ind = ind.size();
		for (int k = 1; k < size_ind; ++k) blens.push_back(ind[k] - ind[k - 1]);
 
		sort(blens.begin(), blens.end());
		int m = blens.size();
		// O(n*sqrt(n))
		for (int k = 0; k < m; ) {
			int r = k;
			while (r < m && blens[r] == blens[k]) ++r;
			memset(dp, 0, sizeof(dp));
 
			for (int i = blens[k]; i <= n; ++i) {
				if (!vis[i] && vis[i - blens[k]] && dp[i - blens[k]] < r - k) {
					dp[i] = dp[i - blens[k]] + 1;
					vis[i] = true;
				}
			}
			k = r;
		}
		/*
		O(n*n)
		for (int i = 0; i < m; ++i)
			for (int j = n; j >= blens[i]; --j)
				vis[j] |= vis[j - blens[i]];
		*/
 
		cout << (vis[n] ? "YES" : "NO") << endl;
	}
 
 
    return 0;
}

战绩

在这里插入图片描述感觉C1能AC但是没想到思路,而且感觉信心也不足,嗨,菜是原罪/(ㄒoㄒ)/~~。
打算长期更新codeforce、leetcode、牛客竞赛,自己还只是个练习时长一年不到的练习生,欢迎同学们交流讨论~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值