【题解】Educational Codeforces Round 153(CF1860)

发布时间 2023-09-02 16:08:12作者: linyihdfj

每次打都想感叹一句,Educational 名不虚传。

A.Not a Substring

题目描述:

\(t\) 组数据,对于每一组数据,你需要判断能否构造一个只由左右括号组成且长度为已经给定字符串的 \(2\) 倍且已经给定的字符串不是子串的合法字符串。注:合法的字符串是左右括号能完全匹配的字符串。如果能,输出 YES ,并同时给出一个合法的答案,如果不能,则输出 NO

题目分析:

发现其实样例的很多字符串都含有 \())\) 或者 \(((\) 这种结构,考虑如果有这种结构我们显然可以直接 \(()()()()\cdots\) 来构造答案。
而如果不含有这种结构就必然是 \(()()()(\) 等类似的东西,这个时候可以直接 \(((((\cdots))))\) 来构造答案。
需要特判 \(()\) 的情况为无解。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1000;
char s[N];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		scanf("%s",s+1);
		int n = strlen(s+1);
		if(n == 2 && s[1] == '(' && s[2] == ')'){
			printf("NO\n");
			continue;
		}
		bool flag = false;
		for(int i=1; i<n; i++){
			if(s[i] == s[i+1])	flag = true;
		}
		printf("YES\n");
		if(flag){
			for(int i=1; i<=n; i++)	printf("()");
		}
		else{
			for(int i=1; i<=n; i++)	printf("(");
			for(int i=1; i<=n; i++)	printf(")");
		}
		printf("\n");
	}
	return 0;
}

B.Fancy Coins

题目描述:

\(t\) 组数据:对于每组数据,你有 \(a_1\)\(1\) 元的普通硬币和 \(a_k\)\(k\) 元的普通硬币,同时你还有无限的花硬币(面值 \(1\) 元和 \(k\) 元均有),你需要用最少的花硬币组成恰为 \(m\) 元的面值,输出花硬币使用的最小值。

题目分析:

一个显然的想法就是花硬币肯定要尽可能地使用面值为 \(k\) 的硬币,所以就可以发现面值为 \(1\) 的硬币的作用仅仅只是让 \(m\) 变成 \(k\) 的倍数(这里默认将选择硬币的贡献变为将 \(m\) 减少 \(1\)\(k\))。
所以过程就是先用 \(1\) 的原有硬币和花色硬币将 \(m\) 变成 \(k\) 的倍数,然后再用剩下的 \(k\) 的硬币和 \(1\) 的硬币将 \(m\) 变成 \(0\) 即可。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
	int T;scanf("%lld",&T);
	while(T--){
		int m,k,a,b;scanf("%lld%lld%lld%lld",&m,&k,&a,&b);
		int tmp1 = m - m / k * k;
		int ans = 0;
		if(a < tmp1)	ans += tmp1 - a,a = 0;
		else	a -= tmp1;
		b += a/k;
		tmp1 = m / k;
		if(b < tmp1)	ans += tmp1 - b;
		printf("%lld\n",ans);
	}
	return 0;
}

C.Game on Permutation

题目描述:

两人在一个长为 \(n\) 排列 \(p\) 上玩游戏,先手初始可以任意选择一个位置(注意先手不能选择一个不能移动的位置),然后他们以先手选择的位置为起点后手先依次交替移动,每次只能向左移动到比当前数字小的位置(即如果 \(i<j\)\(p_i<p_j\) ,则可从 \(j\) 移动到 \(i\) ),如果一个人不能移动,则此人获胜,请问先手开始选择有必胜策略的位置的个数。

题目分析:

一个想法就是直接求解 SG 函数,但是因为没有任何后继状态的时候对应的答案为先手必胜,所以看上去 SG 函数的值和必胜必败没有什么必然的联系。
这样其实就是想复杂了,我们可以直接记 \(f_i\) 表示第 \(i\) 个位置是否先手必胜(因为 Alice 第一步为放置到 \(i\),所以此时的先手其实是 Bob),根据博弈的常识只要 \(i\) 的后继状态中存在先手必败则 \(f_i = 1\) 否则 \(f_i = 0\),也就是若存在 \(j < i\)\(p_j < p_i\) 而且 \(f_j = 0\)\(f_i = 1\) 否则 \(f_i = 0\)
这个东西就是一个二维数点,放到 BIT 上查询区间最小值即可,需要特判没有后继状态的答案为先手必胜即可。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int n,mn[N],p[N],f[N];
int lowbit(int x){
	return x & (-x);
}
void modify(int now,int val){
	for(;now <= n; now += lowbit(now))	mn[now] = min(mn[now],val);
}
int query(int now){
	int ans = 1;
	for(; now; now -= lowbit(now))	ans = min(ans,mn[now]);
	return ans;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	mn[i] = 1,f[i] = 0;
		for(int i=1; i<=n; i++)	scanf("%d",&p[i]);
		f[1] = 1;modify(p[1],f[1]);
		int pre = p[1];
		for(int i=2; i<=n; i++){
			int tmp = query(p[i] - 1);
			if(tmp == 0)	f[i] = 1;
			else	f[i] = 0;
			if(p[i] < pre)	f[i] = 1;
			pre = min(pre,p[i]);
			modify(p[i],f[i]);
		}
		int ans = 0;
		for(int i=1; i<=n; i++)	ans += f[i];
		printf("%d\n",n - ans);
	}
	return 0;
}

D.Balanced String

题目描述:

给你一个长为 \(n\)\(01\) 字符串,每次操作交换两个元素,求最少几次操作能使字符串的顺序对 \(01\) 个数与逆序对 \(10\) 个数相等,注意此处不要求两个位置相邻。
\(3 \le n \le 100\)

题目分析:

做法一:
这个东西感觉不是很能维护,所以考虑能不能发现一些性质。
看到这个 \(a = b\) 的要求一个想法就是:是不是 \(a + b\) 为定值,这样只需要 \(a,b\) 等于一个定值就看上去好做很多。
发现的确是定值,这个值就是 \(cnt_0 \times cnt_1\),因为 \(0,1\) 间任意的顺序均可造成 \(1\) 的贡献。
考虑只将 \(01\) 的答案变为这个定值,我们可以考虑在 \(1\) 的位置累加答案,一个 \(1\) 造成的贡献就是前面 \(0\) 的个数。
所以就有了一个很显然的 \(dp\) 状态:设 \(f[i][j][k]\) 表示放了 \(i\)\(0\)\(j\)\(1\),现在 \(01\) 的个数为 \(k\) 的最小交换次数。
转移的话显然就是枚举最后一个位置是 \(0\) 还是 \(1\),但是这个时候最小交换次数该怎么计算呢?
如果直接统计的话会发现根本没法做,因为无法确定这个位置现在有没有被交换过,所以可以考虑若交换 \((x,y)\) 则在 \(x,y\) 处都算 \(1\) 的次数,这样将最后的答案除以 \(2\) 即可。
经过这样的转化之后那么就只需要判断字符串上 \(i+j\) 的位置是不是我们放的这个值即可,如果是则不会有影响,如果不是则交换次数加 \(1\)

如果实现精细一点的话这个 \(dp\) 的状态为 \(O(n^3)\) 转移为 \(O(1)\) 复杂度 \(O(n^3)\)
但是也可以直接滚动数组就不用精细实现了。

做法二:
(其实两个做法差不多,只不过一开始思考方式有些区别)
\(01\) 的个数为 \(A\)\(10\) 的个数为 \(B\)
那么对于 \(A = B\) 这个条件一个经典的转化就是 \(A - B = 0\),这样我们就可以直接维护 \(A - B\) 的值而不用维护相等这个离谱的条件。
那么也可以考虑设 \(f[i][j][k]\) 表示现在放了 \(i\)\(0\)\(j\)\(1\)\(A - B = k\) 的最小交换次数。
转移枚举最后一个位置是 \(0\) 还是 \(1\),这样的对 \(A - B\) 的值的贡献就很好算了,而最小交换次数的更新可以同做法一。
最后我们的答案就是 \(f[cnt_0][cnt_1][0]\),也要注意这个状态中 \(k\) 可能为负,所以要记录一下偏移量或者用 map 维护。

代码:

做法一:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 100;
int f[2][N][N * N],a[N],cnt[2];
char s[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%s",s+1);
	int n = strlen(s+1);
	for(int i=1; i<=n; i++){
		if(s[i] == '0')	cnt[0]++;
		else	cnt[1]++;
	}
	memset(f,0x3f,sizeof(f));
	for(int i=0; i<=cnt[0]; i++){
		int now = i & 1,lst = now ^ 1;
		memset(f[now],0x3f,sizeof(f[now]));
		if(i == 0)	f[now][0][0] = 0;
		for(int j=0; j<=cnt[1]; j++){
			for(int k=0; k<=cnt[0]*cnt[1]; k++){
				if(j - 1 >= 0 && k - i >= 0)	f[now][j][k] = min(f[now][j][k],f[now][j-1][k-i] + (s[i+j] == '0'));
				if(i >= 1)	f[now][j][k] = min(f[now][j][k],f[lst][j][k] + (s[i+j] == '1'));
			}
		}
	} 
	printf("%d\n",f[cnt[0]&1][cnt[1]][cnt[0]*cnt[1]/2]/2);
	return 0;
}

E.Fast Travel Text Editor

题目描述:

给出一个字符串 \(s\) (全为小写字母,长度为 \(n\) )。有一个光标在任意两个字符之间(不能出现在第一个字符之前和最后一个字符之后)。

有三种操作,代价均为 \(1\) :

  • 将光标向左移一位(不能出现在第一个字符之前)。
  • 将光标向右移一位(不能出现在最后一个字符之后)。
  • 若有两对相同的字符对,光标便可从一个字符对之间移动到另一个字符对之间。

\(m\) 组询问。每组询问给出两个整数 \(f\)\(t\) ,求光标从 \(f\) 移动到 \(t\) 的最小代价。
\(2 \le |s| \le 5\times10^4,1 \le m \le 5 \times 10^4\)

题目分析:

这个问题显然是一个最短路问题,我们可以显然根据这三种操作分别建边:\((i,i-1,1)\)\((i,i+1,1)\) 以及对于 \(s_i = s_j,s_{i+1} = s_{j+1}\) 建图 \((i,j,1)\)
因为边权均为 \(1\) 所以可以直接跑 bfs,但是主要到我们 bfs 的复杂度其实是 \(O(|E|+|V|)\) 前两种边数均为 \(O(n)\) 但是第三种边数为 \(O(n^2)\) 直接做复杂度就爆炸了。
这种建图一个常见的优化就是对于每一个字符对建立一个点,然后对于 \(i\) 向代表 \((s_i,s_{i+1})\) 的点连边边权为 \(1\),并从代表的点向 \(i\) 连边边权为 \(0\),这样就可以实现和上面这种建图一样的效果。
这样的复杂度就优化到了 \(O(nm)\),还是不能通过,因为注意到我们每一次询问都跑一次 bfs 显然太亏了,能不能预处理什么东西呢。
因为从 \(i\)\(j\) 如果完全不经过第三种边那么答案就是 \(|i-j|\) 很好做,而如果经过就意味着必然经过某个代表点。
而注意到我们代表点的数量只有 \(26^2 = 676\) 远小于 \(m\),所以可以对于每一个代表点跑一边它到其他点的最短路,然后 \(i\)\(j\) 的最短路就可以通过枚举中间的代表点然后前后拼起来,要注意的是我们这个图本质上是一个有向图,所以前后拼接的时候要一部分为原图另一部分为反图。
我的代码在 CF 上能过,但是需要跑 \(4.6s\),本题时限 \(5s\)
看上去就有点炸裂,但是我们注意到直接用原图的前后拼接问题好像也不是很大,因为除了进出代表节点这里的边的权值会影响一下答案(按我的写法应该加 \(1\) 才可以消掉影响,因为实际只是进入该节点的边的权值被忽略了),其它的建反图之后边权和毫无影响,所以就可以省掉建反图,这样就只要跑 \(3s\) 了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 6e4+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[4 * N];
int cnt,head[N],dis1[30*30][N],id[N],pos[30][30];
char s[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
}
void bfs(int *dis,int s){
	dis[s] = 0;
	deque<int> q;q.push_front(s);
	while(!q.empty()){
		int now = q.front();q.pop_front();
		for(int i=head[now]; i; i=e[i].nxt){
			int to = e[i].to;
			if(dis[to] > dis[now] + e[i].val){
				dis[to] = dis[now] + e[i].val;
				if(e[i].val == 0)	q.push_front(to);
				if(e[i].val == 1)	q.push_back(to);
			}
		}
	}
}
int main(){
	scanf("%s",s+1);
	int tot = 0;
	for(int i=0; i<=25; i++){
		for(int j=0; j<=25; j++){
			pos[i][j] = ++tot;
		}
	}
	int n = strlen(s+1);
	for(int i=1; i<n; i++)	id[i] = ++tot;  //正图 
	for(int i=1; i<n; i++){
		add_edge(id[i],id[i+1],1);add_edge(id[i+1],id[i],1);
		add_edge(id[i],pos[s[i]-'a'][s[i+1]-'a'],1);
		add_edge(pos[s[i]-'a'][s[i+1]-'a'],id[i],0);
	}
	memset(dis1,0x3f,sizeof(dis1));
	for(int i=0; i<=25; i++){
		for(int j=0; j<=25; j++){
			bfs(dis1[pos[i][j]],pos[i][j]);
		}
	}
	int m;scanf("%d",&m);
	while(m--){
		int s,t;scanf("%d%d",&s,&t);
		int ans = abs(s - t);
		for(int i=0; i<=25; i++){
			for(int j=0; j<=25; j++){
				ans = min(ans,dis1[pos[i][j]][id[s]] + dis1[pos[i][j]][id[t]] + 1);
				//从 A -> B 和从 B -> A 其实本质相同
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

F.Evaluate RBS

题目描述:

现有 \(2n\) 个形如 \((a, b, c)\) 的三元组,其中 \(a, b\) 均为正整数, \(c\) 是字符 ()(左圆括号或右圆括号)。其中恰有 \(n\) 个三元组的 \(c\)(,另外 \(n\) 个为 )

对于两个正实数 \(x,y\) ,记一个三元组的特征值为 \(\lambda = ax+by\) 。你需要选择 \(x,y\) 并对三元组按其特征值升序排序。若多个三元组的特征值相同,可以选择随意排列。

问,是否存在一种选法,使得排序后的序列能让 \(c\) 组成一个合法的括号序列。
\(n \le 1500\)

题目分析:

看到 \(ax + by\) 这种形式一个经典的想法就是转化为比较 \(a + b \times \frac{y}{x}\),这样我们相当于只需要考虑 \(t = \frac{y}{x}\) 而不用在意 \(x,y\) 具体是啥。
也就是可以看成一条与 \(y\) 轴截距为 \(a\) 斜率为 \(b\) 的直线,这样随着 \(t\) 的增大必然存在一个位置 \(t'\) 使得 \(t'\) 之前 \((a_1,b_1)\) 优于 \((a_2,b_2)\) 而在 \(t'\) 之后 \((a_1,b_1)\) 劣于 \((a_2,b_2)\)
显然若对于 \((a_1,b_1)\)\((a_2,b_2)\) 存在这样的位置,也就是必然满足 \(a_1 > a_2,b_1 < b_2\),此时推推就可以知道 \(t' = \frac{a_1-a_2}{b_2-b_1}\)
或者如果相反即之前劣于之后优于也行,会发现 \(t'\) 的值也是这个式子。
所以其实我们关心的 \(t\) 的值只有 \(O(n^2)\) 个,也就是只有这些值会直接造成三元组特征值大小关系的变化。
所以就可以考虑直接维护出这 \(O(n^2)\) 个值(这里其实是直接维护的分子和分母,这样没有误差),然后从左到右扫描线,每次将会更改大小关系的三元组拿出来更改大小关系,对于特征值相同的三元组为了让它尽可能地形成合法括号序列,则显然应当让左括号放在左边,右括号放在右边。
快速判断合法括号序列可以考虑经典做法:将左括号记为 \(1\) 将右括号记为 \(-1\),若对于任意位置序列的前缀和均大于 \(0\) 则为合法括号序列,这里显然只需要维护一下更改的影响即可。

代码:

这里贴一下实现十分高妙的题解代码

点击查看代码
#include <bits/stdc++.h>

#define forn(i, n) for (int i = 0; i < int(n); i++)

using namespace std;

const int INF = 1e9;

struct bracket{
	int a, b, c;
};

struct point{
	int x, y;
};

long long cross(const point &a, const point &b){
	return a.x * 1ll * b.y - a.y * 1ll * b.x;
}

long long dot(const point &a, const bracket &b){
	return a.x * 1ll * b.a + a.y * 1ll * b.b;
}

bool operator <(const point &a, const point &b){
	return cross(a, b) > 0;
}

int main() {
	int t;
	cin >> t;
	while (t--){
		int n;
		cin >> n;
		vector<bracket> val;
		forn(i, 2 * n){
			int a, b;
			string c;
			cin >> a >> b >> c;
			val.push_back({a, b, c[0] == '(' ? 1 : -1});
		}
		map<point, vector<int>> opts;
		forn(i, 2 * n) forn(j, 2 * n){
			int dx = val[i].b - val[j].b;
			int dy = val[j].a - val[i].a;
			if (dx <= 0 || dy <= 0) continue;
			opts[{dx, dy}].push_back(i);
			opts[{dx, dy}].push_back(j);
		}
		opts[{1, INF}];
		vector<int> ord(2 * n), rord(2 * n);
		iota(ord.begin(), ord.end(), 0);
		sort(ord.begin(), ord.end(), [&](int i, int j){
			long long di = dot({INF, 1}, val[i]);
			long long dj = dot({INF, 1}, val[j]);
			if (di != dj) return di < dj;
			return val[i].c > val[j].c;
		});
		forn(i, 2 * n) rord[ord[i]] = i;
		int neg = 0, cur = 0;
		vector<int> bal(1, 0);
		for (int i : ord){
			cur += val[i].c;
			bal.push_back(cur);
			neg += cur < 0;
		}
		bool ans = neg == 0;
		vector<int> prv;
		for (auto it : opts){
			vector<int> tot = prv;
			for (int x : it.second) tot.push_back(x);
			sort(tot.begin(), tot.end(), [&](int i, int j){
				return rord[i] < rord[j];
			});
			tot.resize(unique(tot.begin(), tot.end()) - tot.begin());
			for (int x : tot) neg -= bal[rord[x] + 1] < 0;
			vector<int> tmp = tot;
			sort(tot.begin(), tot.end(), [&](int i, int j){
				long long di = dot(it.first, val[i]);
				long long dj = dot(it.first, val[j]);
				if (di != dj) return di < dj;
				return val[i].c > val[j].c;
			});
			vector<int> nrord(tot.size());
			forn(i, tot.size()) nrord[i] = rord[tmp[i]];
			forn(i, tot.size()) rord[tot[i]] = nrord[i];
			for (int x : tot){
				bal[rord[x] + 1] = bal[rord[x]] + val[x].c;
				neg += bal[rord[x] + 1] < 0;
			}
			if (neg == 0){
				ans = true;
				break;
			}
			prv = it.second;
		}
		puts(ans ? "YES" : "NO");
	}
	return 0;
}