2024-09-11-ABC370

A - Raise Both Hands

主要是读题。

输入两个数,10输出yes,01输出no,11或者00输出invalid

#include <bits/stdc++.h>
using namespace std;
int main() {
	
	int L, R;
	cin >> L >> R;
	
	if (L == 1 && R == 0) {
		cout << "Yes\n";
	} else if (L == 0 && R == 1) {
		cout << "No\n";
	} else {
		cout << "Invalid\n";
	}
	
	return 0;
}

B - Binary Alchemy

给定物品合成成分表aij表示物品i和物品j合成物品 aij。

问物品1,依次与 1,2,3,..N物品合成,问最后的物品。

4
3
2 4
3 1 2
2 1 2 4
2

按照题意模拟即可

#include <bits/stdc++.h>
using namespace std;
int main() {
	
	int N;
	cin >> N;
	
	vector A(N, vector<int>(N));
	for (int i = 0; i < N; i++) {
		for (int j = 0; j <= i; j++) {
			cin >> A[i][j];
			A[i][j]--;
			A[j][i] = A[i][j];
		}
	}
	
	int x = 0;
	for (int i = 0; i < N; i++) {
		x = A[x][i];
	}
	
	cout << x + 1 << "\n";
	
	return 0;
}

C - Word Ladder

有两个长度相等的字符串S和T,每次操作可以更改S中的一个字母,并将当前这次操作后的字符串S加入到一个新的字符串数组X的末尾。问最小的操作次数,并在最小的操作次数下,找出字典序最小的X。

adbe
bcbc
3
acbe
acbc
bcbc

首先最小的操作次数是一定的,就是两个字符串的不同的字母个数。

然后考虑修改字母的顺序来使X的字典序尽可能的小,一些字母是Si>Ti,一些字母是Si<Ti,即一个改了之后字典序变小了,一个改了之后字典序变大了,那么要使序列X的字典序尽可能小,那肯定是要先让S的字典序降下来,也就是改尽可能靠前的,能使字典序变小的字母,然后再改尽可能靠后的,会使字典序变大的字母,这个序列才是最佳的序列。

#include <bits/stdc++.h>
using namespace std;
int main() {
	
	string S, T;
	cin >> S >> T;
	
	vector<string> ans;
	const int N = S.size();
	for (int i = 0; i < N; i++) {
		if (S[i] > T[i]) {
			S[i] = T[i];
			ans.push_back(S);
		}
	}
	for (int i = N - 1; i >= 0; i--) {
		if (S[i] < T[i]) {
			S[i] = T[i];
			ans.push_back(S);
		}
	}
	
	cout << ans.size() << "\n";
	for (const auto &s : ans) {
		cout << s << "\n";
	}
	
	return 0;
}

D - Cross Explosion

二维网格,初始每个格子有墙。

依次进行q次放炸弹的操作,给定每次放炸弹的位置(i,j),如果该位置有墙,则该墙消失。

否则,炸弹会爆炸,会产生十字冲击波,该位置上下左右的各第一个墙都会消失。

问最后还存在的墙的数量。

题目已知H*W并不大,所以可以开二维数组记录每一个位置是否有炸弹(也可以不开)。

然后问题就是如何快速找到一个位置的上下、左右的第一个墙,不难想到对于每一行和每一列开一个set,利用set的upperbound找到离这个位置最近的墙,然后利用set的erase删除即可。

#include <bits/stdc++.h>
using namespace std;
int h, w, q;
set<int> row[400005], col[400005];
int main() {
	cin >> h >> w >> q;
	for (int i = 1; i <= h; i++) {
		for (int j = 1; j <= w; j++) {
			row[i].insert(j);
			col[j].insert(i);
		}
	}
	while(q--)
	{
		int x,y;
		cin>>x>>y;
		if(*row[x].lower_bound(y)==y) {
			row[x].erase(y);
			col[y].erase(x);
		} else {
			auto it=row[x].lower_bound(y);
			if(it!=row[x].end()) {
				col[*it].erase(x);
				row[x].erase(it);
			}
			it=row[x].lower_bound(y);
			if(it!=row[x].begin()) {
				it--;
				col[*it].erase(x);
				row[x].erase(it);
			}
			
			it=col[y].lower_bound(x);
			if(it!=col[y].end()) {
				row[*it].erase(y);
				col[y].erase(it);
			}
			it=col[y].lower_bound(x);
			if(it!=col[y].begin()) {
				it--;
				row[*it].erase(y);
				col[y].erase(it);
			}
		}
	}
	int ans=0;
	for(int i=1;i<=h;i++) ans+=row[i].size();
	cout<<ans;
	return 0;
}

E - Avoid K Partition

给定一个数组a,划分成若干个子区间,使得没有子区间的和为k。

求划分方案数。

考虑朴素的dp,dpi表示前i个数字的划分方案数,那么转移肯定是枚举上一段的位置:

image-20240909183107763

这个方法是n方的,难点在于最后一段的和不能等于k,不然的话dpi直接就等于dp的前缀和了。

如果是前缀和的话,则表示最后一段的和为任意值都可以的总方案数,我们要想办法减掉最后一段和为k的方案数,那么我们反过来思考,设a1~ai的总和为s,最后一段要是k,那么之前的和就是s-k,所以其实就是剪掉前缀的和为s-k的方案数,也就是维护一个cnt[i]表示前缀和为i的方案数,这个开个map记一下就好了。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int mo = 998244353;
int main() {
	int n;
	LL k;
	cin >> n >> k;
	vector<int> a(n);
	for(int i=0;i<n;i++) 
		cin>>a[i];
	map<LL, int> cnt;
	cnt[0] = 1;
	LL presum = 0;
	int precnt = 1;
	int ans = 0;
	for (auto& i : a) {
		presum += i;
		ans = (precnt - cnt[presum - k] + mo) % mo;
		cnt[presum] = (cnt[presum] + ans) % mo;
		precnt = (precnt + ans) % mo;
	};
	cout << ans << '\n';
	return 0;
}

F - Cake Division

给定一个环形数组,划分为k段,使得每段和的最小值最大。

在该最大值的各种划分方案中,求有多少位置,在所有划分方案中都不被分开。

5 2
3 6 8 6 4
13 1

这题通过的人很少。

我们在之前的abc讲过,遇到这种环形的问题,我们可以把数组复制一遍接在后面。

首先考虑怎么求得最大的区间和最小值,设这个区间和为x,那么也就是至少有一种合法的划分方案(即划分段数为k),使其中的每一段和都大于等于x。那么我们不难发现x是具有单调性的,x小的时候这个条件很容易满足,而x超过一个值之后就不能达到这个目标了,我们要求的就是这个值,所以这显然是一个二分答案。

然后考虑二分出x后,怎么检查这个加粗的条件是否可以达到,一个简单的方法是贪心:

我们首先计算一个数组fi表示从第i个数出发,满足和刚好大于x的区间的右端点,这个数组可以使用滑动窗口来完成。

比如:2 9 8 1 7 9 1 3 5 8 , x=13

那么我们首先复制一遍数组2 9 8 1 7 9 1 3 5 8 2 9 8 1 7 9 1 3 5 8,然后使用滑动窗口计算出f数组2 3 4 5.......

有了这个数组之后,我们的主要思想是刚大于等于x时就分段(因为没必要再加多余的数进当前段,把数留给后面的段肯定更好),所以我们只要枚举一个起点,然后每次通过f数组向后跳转到每一个段的右端点,如果跳了k段后,我们当前的右端点到左端点的距离是大于等于n的(即跑了一圈),那么从当前起点开始划分的方案就是可行的,当前的x也是可行的。枚举起点是O(n),跳一圈是O(k),所以检查的时间复杂度为O(nk),还无法通过这道题。

那么遇到这种“跳”的问题,我们可以联想到倍增,因为一个一个跳右端点实在是太慢了。设数组jumpij表示从第i个点,向后的第2^j个段的右端点位置,jump数组可以这么得到:

jump[i][0]=f[i]//f就是往后一个区间,所以就是2的0次方
jump[i][j]=jump[jump[i][j-1]][j-1]//相当于想要得到2的j次方,其实就是向后先跳2的j-1次方,再从这个位置在往后跳2的j-1次方

有了jump数组之后,往后跳k个区间只要把k拆成二进制,然后一个一个跳就行了,这样时间复杂度就缩小到了O(n log k),可以通过了。

然后还问了有多少个点是不会被断开的,这个比较简单,当以当前点为起点时是不可行的,但是在同一个x下有可行的,当前点就是不能被断开的,统计一下有多少个不可行的起点即可。

#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int a[400005], N, n, k;
int up[400005][30];
int check (int x) {
	memset(up,0,sizeof(up));
	up[N][0] = N + 1;
	up[N + 1][0] = N + 1;
	queue<int> windows;
	int r = 0;
	int sum = 0;
	for (int i = 0; i < N; ++i) {
		while (r < N && sum < x) {
			windows.push(a[r]);
			sum += a[r];
			++r;
		}
		if (sum < x)
			up[i][0] = N + 1;
		else
			up[i][0] = r;
		
		sum -= windows.front();
		windows.pop();
	}
	for (int i = 1; i < 20; ++i)
		for (int j = 0; j < N + 2; ++j) {
			up[j][i] = up[up[j][i - 1]][i - 1];
		}
	int cnt = 0;
	for (int i = 0; i < n; ++i) {
		int pos = i;
		for (int j = 0; j < 20; ++j) {
			if (k & (1 << j)) {
				pos = up[pos][j];
			}
		}
		if (pos <= i + n) {
			++cnt;
		}
	}
	return cnt;
}
int main() {
	cin >> n >> k;
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
		a[i + n] = a[i];
	}
	N = n + n;
	int l = 1, r = 2e9 + 8;
	
	while (l + 1 < r) {
		int mid = l + (r - l) / 2;
		if (check(mid))
			l = mid;
		else
			r = mid;
	}
	int cnt = check(l);
	cout << l << ' ' << n - cnt << '\n';
	
	return 0;
}

posted @ 2024-11-30 23:27  ~清渠~  阅读(1)  评论(0编辑  收藏  举报