Codeforces Round #744 (Div. 3) 题解 完整A~G

Codeforces Round #744 (Div. 3) 题解

A. Casimir’s String Solitaire

题意

给出一个仅由字符’A’,‘B’,‘C’构成的字符串,每次可以同时消去一个’A’和一个’B’,或同时消去一个’B’和一个’C’,问最终能否消去所有字符。

思路

只需’B’的个数与’A’,'C’的个数之和相等即可全部消除。

时间复杂度

O ( 1 ) O(1) O(1)

AC代码
ProblemLangVerdictTimeMemory
A - Casimir’s String SolitaireGNU C++17Accepted15 ms3600 KB
#include <bits/stdc++.h>

using namespace std;

char s[55];

void solve() {
	scanf("%s", s);
	int n = strlen(s);		//提前求出字符数量,可降低复杂度,但将strlen放到循环中也可以通过本题
	int a = 0, b = 0;		//a记录'A'与'C'的个数之和,b记录'B'的个数
	for (int i = 0; i < n; ++i) {
		if (s[i] == 'B') ++b;
		else ++a;
	}
	if (a == b) printf("YES\n");
	else printf("NO\n");
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}

B. Shifting Sort

题意

给出一个整数序列,每次可以选取一个区间 [ l , r ] [l,r] [l,r],选取一个正整数 d d d,并对区间 [ l , r ] [l,r] [l,r]循环左移 d d d个位置,要求在至多 n n n次操作后使序列从小到大有序,需要输出每一次操作。

思路

我们可以使用选择排序法,选取最小值,将其左移至第一个位置,然后在剩余的数中选取最小值,左移至第二个位置,依次类推即可完成。需要注意的是,如果最小值已经位于首位,则不需要任何操作。

时间复杂度

O ( n 2 ) O(n^2) O(n2)

AC代码
ProblemLangVerdictTimeMemory
B - Shifting SortGNU C++17Accepted31 ms3600 KB
#include <bits/stdc++.h>

using namespace std;

struct dt {
	int l, r, d;

	dt(int ll, int rr, int dd) {	//构造函数,用于emplace_back
		l = ll, r = rr, d = dd;		//不写构造函数,换成push_back也可以做
	}
};

int a[55], b[55];

void solve() {
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	vector<dt> v;		//记录操作
	for (int i = 1; i <= n; ++i) {
		int mn = i;		//记录最小值的位置
		for (int j = i + 1; j <= n; ++j) if (a[j] < a[mn]) mn = j;
		if (mn == i) continue;	//最小值恰好是首个
		int d = mn - i;
		v.emplace_back(i, n, d);	//记录操作
		for (int j = i; j <= n; ++j) {		//模拟循环移动,更新数组
			if (j - d >= i) b[j - d] = a[j];
			else b[n + 1 + j - d - i] = a[j];
		}
		for (int j = i; j <= n; ++j) a[j] = b[j];
	}
	printf("%d\n", v.size());
	for (auto &i: v) printf("%d %d %d\n", i.l, i.r, i.d);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}

C. Ticks

题意

给出一个 n n n m m m列的方格阵列,定义图案“对勾”:对 ∀ h ∈ [ 0 , d ] \forall h\in[0,d] h[0,d],坐标 ( i − h , j ± h ) (i-h,j\pm h) (ih,j±h)的方格均被涂黑,则称之为以 ( i , j ) (i,j) (i,j)为中心,高为 d d d的“对勾”。问给定的方格阵列能否由若干个高度不小于 k k k的“对勾”组成(不同对勾允许有重叠)。

思路

存在中心位于 ( i , j ) (i,j) (i,j),高为 d d d的“对勾”的充分必要条件是:从 ( i , j ) (i,j) (i,j)开始,在左上和右上方有至少 d d d个连续的黑色格子。同样,某个黑色格子属于某个“对勾”的充分必要条件是:从该位置 ( x , y ) (x,y) (x,y)开始向左下和右下方向寻找,能找到一个位置 ( i , j ) (i,j) (i,j),使得存在一个中心位于 ( i , j ) (i,j) (i,j),高不少于 x − i x-i xi的“对勾”。因此,只需暴力预处理每个位置向左上、右上方向最长的连续黑格子数量,再暴力判断是否满足条件即可。

时间复杂度

O ( n 3 ) O(n^3) O(n3)

AC代码
ProblemLangVerdictTimeMemory
C - TicksGNU C++17Accepted31 ms3700 KB
#include <bits/stdc++.h>

using namespace std;

char s[20][20];
int a[20][20], b[20][20];		//数组a记录左上方最长连续黑格子数,数组b记录右上方的
int n, m, k;

bool check() {
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < m; ++j) {
			if (s[i][j] == '.') continue;	//只针对黑色方格进行判断,白色方格直接忽略
			bool ok = false;
			int x, y, cnt;
			x = i, y = j, cnt = 0;
			while (x < n && y < m) {
				if (a[x][y] >= max(k, cnt) && b[x][y] >= max(k, cnt)) {		//左上右上需同时满足
					ok = true;
					break;
				}
				++cnt, ++x, ++y;
			}
			if (ok) continue;
			x = i, y = j, cnt = 0;
			while (x < n && y >= 0) {
				if (a[x][y] >= max(k, cnt) && b[x][y] >= max(k, cnt)) {
					ok = true;
					break;
				}
				++cnt, ++x, --y;
			}
			if (!ok) return false;
		}
	}
	return true;
}

void solve() {
	scanf("%d%d%d", &n, &m, &k);
	for (int i = 0; i < n; ++i) scanf("%s", s[i]);
	memset(a, 0, sizeof a);
	memset(b, 0, sizeof b);
	for (int i = 0; i < n - 1; ++i)		//向右下方向递推,求出左上方向连续黑格子数
		for (int j = 0; j < m - 1; ++j)
			if (s[i][j] == '*' && s[i + 1][j + 1] == '*') a[i + 1][j + 1] = a[i][j] + 1;
	for (int i = 0; i < n - 1; ++i)		//向左下方向递推,求出右上方向连续黑格子数
		for (int j = m - 1; j > 0; --j)
			if (s[i][j] == '*' && s[i + 1][j - 1] == '*') b[i + 1][j - 1] = b[i][j] + 1;
	if (check()) printf("YES\n");
	else printf("NO\n");
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

D. Productive Meeting

题意

会议上有 n n n个人,第 i i i个人最多愿意讨论 a i a_i ai次,任意两个人都可以讨论任意次数。题目要求找出最大的讨论次数,并输出讨论的方案。

思路

贪心地找出当前愿意讨论次数最多的两个人,并使这两个人讨论一次,重复这一操作,直到无法再找到愿意讨论的两个人为止,即可完成此题。为了找到次数最多的人,使用堆(优先队列)即可。

时间复杂度

O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

AC代码
ProblemLangVerdictTimeMemory
D - Productive MeetingGNU C++17Accepted109 ms7300 KB
#include <bits/stdc++.h>

using namespace std;

typedef pair<int, int> pii;

void solve() {
	int n;
	scanf("%d", &n);
	priority_queue<pii> q;		//大顶堆
	for (int i = 1; i <= n; ++i) {
		int p;
		scanf("%d", &p);
		if (p) q.emplace(p, i);
	}
	vector<pii> v;		//用于记录过程
	while (q.size() > 1) {
		pii a = q.top();		//找到堆顶两个元素
		q.pop();
		pii b = q.top();
		q.pop();
		v.emplace_back(a.second, b.second);		//记录一次讨论
		--a.first, --b.first;		//减少可讨论次数
		if (a.first) q.push(a);
		if (b.first) q.push(b);
	}
	printf("%d\n", v.size());
	for (auto &i: v) printf("%d %d\n", i.first, i.second);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}

E1. Permutation Minimization by Deque

题意

给出一个序列,依次加入一个空的双端队列,可以任选从头或尾加入。求可能的字典序最小的结果。请自行了解字典序的含义。

思路

采取贪心策略,出了第一个以外,新加入的元素若大于队头,在加入在队尾,否则加入在队头。模拟即可得到结果。

时间复杂度

O ( n ) O(n) O(n)

AC代码
ProblemLangVerdictTimeMemory
E1 - Permutation Minimization by DequeGNU C++17Accepted109 ms4700 KB
#include <bits/stdc++.h>

using namespace std;

void solve() {
	int n;
	scanf("%d", &n);
	deque<int> q;		//双端队列
	int p;
	scanf("%d", &p);
	q.push_back(p);		//直接加入第一个元素
	for (int i = 1; i < n; ++i) {
		scanf("%d", &p);		//比较后加入其余元素
		if (p > q.front()) q.push_back(p);
		else q.push_front(p);
	}
	for (int i: q) printf("%d ", i);	//输出
	printf("\n");
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}

E2. Array Optimization by Deque

题意

给出一个序列,依次加入一个空的双端队列,可以任选从头或尾加入。求可能的逆序对最少的结果。请自行了解逆序对的含义。

思路

考虑最后一个元素,该元素不论在队头还是在队尾,产生的逆序对数量都与其他元素的顺序无关,因此贪心地选择产生逆序对数量较少的方案即可。随后,处理倒数第二个元素,方法与最后一个元素完全相同。注意:处理倒数第二个元素时,无需考虑它与最后一个元素是否会产生逆序对,因为已经在最后一个元素的计算过程中算过了。如此重复操作,处理所有元素即可。计算产生逆序对的数量,可以使用树状数组。本题数据范围较大,需要先离散化。

时间复杂度

O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

AC代码
ProblemLangVerdictTimeMemory
E2 - Array Optimization by DequeGNU C++17Accepted280 ms11700 KB
#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 5;

int a[N], d[N], cnt;			//cnt记录离散化后的数量

int lowbit(int x) {
	return x & -x;
}

void upd(int x, int k) {		//树状数组单点修改
	for (int i = x; i <= cnt; i += lowbit(i)) d[i] += k;
}

int sum(int x) {				//树状数组区间求和
	int r = 0;
	for (int i = x; i > 0; i -= lowbit(i)) r += d[i];
	return r;
}

void solve() {
	int n;
	scanf("%d", &n);
	map<int, int> m;			//用于离散化
	for (int i = 1; i <= n; ++i) {
		scanf("%d", &a[i]);
		m[a[i]];
	}
	cnt = 0;
	for (auto &i: m) i.second = ++cnt;
	for (int i = 1; i <= n; ++i) a[i] = m[a[i]], upd(a[i], 1);	//离散化,并在树状数组中该位置加1
	long long ans = 0;
	for (int i = n; i >= 1; --i) {
		int l = sum(a[i] - 1), r = sum(cnt) - sum(a[i]);
		if (l > r) ans += r, upd(a[i], -1);
		else ans += l, upd(a[i], -1);
	}
	printf("%lld\n", ans);
	memset(d, 0, (cnt + 5) << 2);		//记得清空树状数组
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}

F. Array Stabilization (AND version)

题意

现有一个仅由0和1组成的数组 a a a,记数组 a a a循环左移 d d d个位置后得到的序列为 a → d a^{\to d} ad,现在对 a a a的每个元素 a i a_i ai,将其替换为 a i & a i → d a_i\&a_i^{\to d} ai&aid,称为一次操作。不断重复操作,直到整个序列不再变化,称作稳定状态。要求判断稳定状态下是否还存在1,若存在,输出-1,否则求出从初始状态到稳定状态需要的操作数。

思路

当且仅当 a i = a ( i − d + n ) % n = 1 a_i=a_{(i-d+n)\%n}=1 ai=a(id+n)%n=1时,经过一次操作后, a i a_i ai仍为1。因此我们只需暴力模拟跳跃 d d d个位置这一操作,找到最长的连续的1的数量,即可求得答案。在此过程中,有一个性质:令 k = g c d ( n , d ) k=gcd(n,d) k=gcd(n,d)(gcd表示最大公约数),则我们可以找到 k k k个独立的循环,每个循环的长度是 n k \frac{n}{k} kn。因此当连续的1的数量达到 n k \frac{n}{k} kn时,说明不论操作多少次,1都会存在。

友情提醒1:注意 d d d的数据范围, d = n d=n d=n是可能的,这将导致某种写法超时,可以通过特判 d = n d=n d=n解决。

友情提醒2:关于上述涉及的“性质”,可以通过离散数学中群和子群的方法严谨表述和证明,在此不作解释。

友情提醒3:本题数据范围很大,解法看似暴力,实则复杂度不大,可以放心写。

时间复杂度

O ( n ) O(n) O(n)

AC代码
ProblemLangVerdictTimeMemory
F - Array Stabilization (AND version)GNU C++17Accepted249 ms11500 KB
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 5;

int a[N], b[N];

void solve() {
	int n, d;
	scanf("%d%d", &n, &d);
	memset(b, 0, (n + 5) << 2);		//不要全memset,容易超时
	for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
	if (d == n) {			//特判d=n
		for (int i = 0; i < n; ++i) {
			if (a[i]) {
				printf("-1\n");
				return;
			}
		}
		printf("0\n");
		return;
	}
	int k = __gcd(n, d), lim = n / k, ans = 0;
	for (int i = 0; i < k; ++i) {
		int pos = i;
		for (int j = 0; j < 2 * lim; ++j) {		//要进行2轮模拟,避免因为首尾相接的问题出错
			if (a[(pos + d) % n]) b[(pos + d) % n] = b[pos % n] + 1;
			if (b[(pos + d) % n] >= lim) {		//一定存在1的情形
				printf("-1\n");
				return;
			}
			pos = (pos + d) % n;
		}
	}
	for (int i = 0; i < n; ++i) ans = max(ans, b[i]);
	printf("%d\n", ans);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}

G. Minimal Coverage

题意

在一个一维的数轴上有若干个线段,其中第一条线段的起点在0位置。给出线段的长度,相邻的线段必须首尾相接,但方向不一定要相同,求这些线段连接起来之后的最小总覆盖长度。

题意比较难懂,用数据举例:线段长度是 [ 7 , 8 , 6 ] [7,8,6] [7,8,6],第一条线段覆盖区间 [ 0 , 7 ] [0,7] [0,7],第二条线段覆盖区间 [ − 1 , 7 ] [-1,7] [1,7],第三条线段覆盖区间 [ − 1 , 5 ] [-1,5] [1,5],则总覆盖为 [ − 1 , 7 ] [-1,7] [1,7],长度为8。

思路

总体思路采用二分策略,对答案进行二分,二分上界是 2 ⋅ m a x   a i 2\cdot max\ a_i 2max ai。对于每一个尝试的值 m i d mid mid,我们可以记录已处理的线段的尾端可能达到的位置,每加入一条新的线段,对可能的位置进行更新。由于该位置离起点不能超过 m i d mid mid个单位,我们需要删去超出范围的位置(更新时不加入即可)。

方法优化1:使用STL容器中的bitset来优化更新可能位置的操作,可以大幅提高效率,并且可以减少代码量(利用左移、右移、与、或运算)。

方法优化2:线段终点可能出现负数,因此直接用数组,或bitset来处理会很麻烦(需要加上一个适当的数,把负数转化为正数),实际上,第一条线段的起点在什么位置并不重要(也就是不一定要从0位置开始),因此不妨将初始状态设置为 m i d mid mid个1。该方法需要一定的理解能力,不能理解的可不采用,下面提供的代码采用了本方法。

时间复杂度

O ( n l o g 2 n ⋅ m a x   a i ) O(nlog_2n\cdot max\ a_i) O(nlog2nmax ai)

AC代码
ProblemLangVerdictTimeMemory
G - Minimal CoverageGNU C++17Accepted46 ms3700 KB
#include <bits/stdc++.h>

using namespace std;

int a[10005];
int n;

bool check(int x) {
	bitset<2005> b, c;
	for (int i = 0; i <= x; ++i) b[i] = c[i] = true;				//左移代表线段方向为负,右移代表正
	for (int i = 0; i < n; ++i) b = (b << a[i] | b >> a[i]) & c;	//&c的目的是去掉超出范围的位置
	return b.any();
}

void solve() {
	scanf("%d", &n);
	for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
	int l = 0, r = 2000, ans;			//二分答案
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (check(mid)) ans = mid, r = mid - 1;
		else l = mid + 1;
	}
	printf("%d\n", ans);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) solve();
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值