【题解】Educational Codeforces Round 152(CF1849)

发布时间 2023-08-31 09:47:09作者: linyihdfj

A.Morning Sandwich

题目描述:

吃货小 C 喜欢三明治,他有三种材料:面包、芝士、火腿。正确的三明治组合应为一层面包和一层芝士或火腿之一轮流放置,以面包为结尾,例如面包-芝士-面包-火腿-面包就是合法的三明治。

给定每次做三明治的材料:$ b $ 面包,$ c $ 芝士,$ h $ 汉堡,求出小 C 最多做多少层三明治。

多测,$ t $ 组数据。

$ 1 \le t \le 1000 , 2 \le b \le 100 , 1 \le c,h \le 100 $。

题目分析:

可以将三明治的结构理解为:一层芝士/汉堡对应一层面包,在最后还要多来一个面包。
所以只需要 \(b-1\)\(c + h\)\(\min\) 后加 \(1\) 即可。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	int T;scanf("%d",&T);
	while(T--){
		int a,b,c;scanf("%d%d%d",&a,&b,&c);
		if(a < 2)	printf("0\n");
		else{
			printf("%d\n",2 * min(a-1,b+c) + 1); 
		}
	}
	return 0;
}

B.Monsters

题目描述:

小 C 在玩一款全新开放世界冒险游戏。

本关卡有 $ n $ 个怪物,小 C 一刀 $ k $ 血,第 $ i $ 怪物初始 $ a_i $ 血。

怪物 0 血或负血会死掉。

小 C 每次打击目前剩余血量最多的怪物,如有多个打击编号最小的。

按照小 C 击杀怪物的顺序输出怪物编号。

多测,$ t $ 组数据。

$ 1 \le t \le 10^4 , 1 \le n \le 3 \times 10^5 , 1 \le k,a_i \le 10^9 $。

题目分析:

考虑这个过程是什么样的。
一样是先通过减 \(k\) 将每一个 \(a_i\) 变为 \(\le k\) 的数,然后一击必杀。
所以就直接对 \(k\) 取模然后按剩下的血量排序即可,注意的细节就是若 \(a_i \mod k = 0\),则相当于减到 \(k\) 而不是 \(0\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 1e6+5;
int a[N];
PII b[N];
bool cmp(PII a,PII b){
	if(a.first != b.first)	return a.first > b.first;
	return a.second < b.second;	
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%d",&T);
	while(T--){
		int n,k;scanf("%d%d",&n,&k);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++){
			if(a[i] % k == 0)	a[i] = k;
			else	a[i] %= k;
		}
		for(int i=1; i<=n; i++)	b[i] = {a[i],i};
		sort(b+1,b+n+1,cmp);
		for(int i=1; i<=n; i++)	printf("%d ",b[i].second);
		printf("\n");
	}
	return 0;
}

C.Binary String Copying

题目描述:

小 C 获得了一个长度为 $ n $ 的 01 串 $ S $。

小 C 有 $ m $ 次操作,每次操作形如 $ [l,r] $,代表将 $ S $ 复制,生成一个 $ S $ 的副本,将副本 $ [l,r] $ 区间内数字字符从小到大排序,第 $ i $ 次操作生成的新串记为 $ S_i $。操作间互不影响,互相独立

求出有多少不同的 $ S_i $。

多测,$ t $ 组数据。

$ 1 \le t \le 10^4 , 1 \le \sum n,\sum m \le 2 \times 10^5 $。

题目分析:

这是一个 \(01\) 串,所以对于 \([l,r]\) 从小到大排序之后其前缀 \(0\) 和后缀 \(1\) 的位置都不会改变,设 \(l_1\) 代表从前到后第一个 \(1\) 的位置 \(r_1\) 代表从后到前第一个 \(0\) 的位置,排序影响的只有 \([l_1,r_1]\)
也就是说我们可以将区间 \([l,r]\) 用区间 \([l_1,r_1]\) 替代,会发现的是当 \([l_1,r_1]\) 相同则字符串相同,当 \([l_1,r_1]\) 不同则字符串不同。
所以对于 \([l_1,r_1]\) 的种类计数即可。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int nxt[N],pre[N];
map<int,int> mp[N];
char s[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T;scanf("%d",&T);
	while(T--){
		int n,m;scanf("%d%d",&n,&m);
		scanf("%s",s+1);
		nxt[n+1] = n+1; 
		for(int i=n; i>=1; i--){
			if(s[i] == '1')	nxt[i] = i;
			else	nxt[i] = nxt[i+1];
		}
		pre[0] = 0;
		for(int i=1; i<=n; i++){
			if(s[i] == '0')	pre[i] = i;
			else	pre[i] = pre[i-1];
		}
		int ans = 0;
		while(m--){
			int l,r;scanf("%d%d",&l,&r);
			l = nxt[l],r = pre[r];
			if(l >= r)	ans += !mp[0][0],mp[0][0] = true;
			else	ans += !mp[l][r],mp[l][r] = true;
		}
		printf("%d\n",ans);
		for(int i=0; i<=n; i++)	mp[i].clear();
	}
	return 0;
}

D.Array Painting

题目描述:

有一个长度为 \(n\) 的数组 \(a\),满足 \(\forall a_i\in\{0,1,2\}\),一开始所有元素均为蓝色。

可以有如下操作:

  • 用一枚硬币,把一个蓝色元素涂成红色;

  • 选择一个不等于 \(0\) 的红色元素和一个与其相邻的蓝色元素,将所选的红色元素减少 \(1\),并将所选的蓝色元素涂成红色。

要将所有元素涂红,最少需要多少硬币?

题目分析:

考虑一次操作最优就是让极长的 \([l,r] (\forall i \in [l,r] , a[i] \ge 1)\) 全部被染红。
若区间最大值为 \(1\) 则可以选择 \(l-1\)\(r+1\) 额外染红。
若区间最大值为 \(2\) 则可以选择 \(l-1\)\(r+1\) 额外染红。
所以就可以考虑将所有的极长段拿出来,先考虑最大值为 \(2\) 的区间再考虑最大值为 \(1\) 的区间染色即可。
如果我们是从左到右依次考虑,那么对于最大值为 \(1\) 的区间优先将 \(l-1\) 染色最优,因为可以给后面更大的发挥空间。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
struct node{
	int l,r,opt;
}b[N];
int a[N];
bool flag[N];
bool cmp(node a,node b){
	if(a.opt != b.opt)	return a.opt > b.opt;
	return a.l < b.l;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;scanf("%d",&n);
	for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
	int tot = 0;
	for(int i=1; i<=n;){
		if(a[i] != 0){
			int pos = i;
			int mx = 0;
			for(;pos <= n && a[pos] != 0; ++pos);
			--pos;
			for(int j=i; j<=pos; j++)	mx = max(mx,a[j]);
			b[++tot] = {i,pos,mx};
			i = pos + 1;
		}
		else	i++;
	}
	sort(b+1,b+tot+1,cmp);
	int ans = 0;
	for(int p=1; p<=tot; p++){
		++ans;
		int l = b[p].l,r = b[p].r;
		for(int i=l;i <= r; i++)	flag[i] = true;
		if(l != 1 && !flag[l-1] && b[p].opt)	b[p].opt--,flag[l-1] = true;
		if(r != n && !flag[r+1] && b[p].opt)	b[p].opt--,flag[r+1] = true;
	}
	for(int i=1; i<=n; i++){
		if(!flag[i])	++ans;
	}
	printf("%d\n",ans);
	return 0;
}

E.Max to the Right of Min

题目描述:

现有一长度为 \(n(n\leq 10^6)\) 的排列,求有多少子区间 \([l,r]\) 满足区间最大值出现在区间最小值的右侧。

题目分析:

做法一:
一个显然的想法就是枚举点东西,第一感觉就是枚举最大值和最小值,然后看贡献是个怎么样的式子,这个式子肯定可以快速对一个区间统计。
\(lmx_i,rmx_i\) 分别表示左边/右边第一个比 \(a_i\) 大的数,\(lmn_i,rmn_i\) 表示左边/右边第一个比 \(a_i\) 小的数。
\(a_i\) 为最大值,\(a_j\) 为最小值,其满足条件的区间的左右端点就是:\(l \in [\max(lmx_i+1,lmn_j+1),j],r \in [i,\min(rmx_i-1,rmn_j-1)]\)
造成的贡献显然就是:\((j - \max(lmx_i+1,lmn_j+1) + 1) * (\min(rmx_i-1,rmn_j-1) - i + 1)\)
这个东西可以对于 \(\max,\min\) 的取值分类讨论,这样就可以转化为三维偏序就可以直接做了。
需要注意如果区间长度 \(< 0\) 则不应统计,而不是作为负数统计。

做法二:
我们不直接枚举最大值和最小值,而是转化为枚举最大值和该区间的左端点/右端点。
设最大值位置为 \(i\),则可能的左端点 \(l \in [lmx_i+1,i]\),可能的右端点 \(r \in [i,rmx_i-1]\)
那么我们到底是枚举左端点还是右端点呢,一个结论就是选择可能的区间长度最小的区间枚举即可。
这样的话考虑一下复杂度是什么,也就是一个点会被枚举到多少次。
若对应的区间为 \([lmx_i+1,rmx_i-1]\) 且我们选择了 \([i,rmx_i-1]\),则对于 \(p \in [i,rmx_i-1]\) 在下一次被枚举到时其区间长度一定不超过 \([i,rmx_i-1]\),因为必然存在 \(a_p < a_i\)\(a_p < a_{rmx_i-1}\),也就是区间长度会减半,所以一个点最多被枚举 \(O(\log n)\) 次,时间复杂度 \(O(n \log n)\)
需要特判枚举的端点为 \(i\) 的情况。

做法三:
我们不搞那么多花里胡哨的,因为要我们统计多少个区间 \([l,r]\) 满足条件就直接考虑枚举 \(r\) 然后判断有多少个 \(l\) 符合条件。
直接维护很难所以考虑是不是可以增量地维护,也就是当 \(r\) 变为 \(r+1\) 时有什么变化,分类讨论:

  • \(a_{r+1}\) 为最大值,则 \(l \in [lmx_{r+1}+1,r]\) 都是合法的
  • \(a_{r+1}\) 为最小值,则 \(l \in [lmn_{r+1}+1,r]\) 都是不合法的
  • \(a_{r+1}\) 不为最大值和最小值,则不会对原来的合法性产生任何影响。

所以就直接使用线段树维护区间赋值 \(01\) 就可以了。
这里会有的疑问就是 \([lmx_{r+1}+1,r]\)\([lmn_{r+1}+1,r]\) 相交的部分怎么办呢。
其实它们不可能相交,考虑一定存在 \(a_r < a_{r+1}\)\(a_r > a_{r+1}\),也就是必然存在 \(lmx_{r+1} = r\)\(lmn_{r+1} = r\),也就是肯定有一个区间为空,就不可能相交。

代码:

(做法三的代码)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
int sum[4 * N],tag[4 * N],a[N],lmn[N],lmx[N];
void update(int now,int now_l,int now_r,int val){
	sum[now] = val * (now_r - now_l + 1);
	tag[now] = val;
}
void pushup(int now){
	sum[now] = sum[now<<1] + sum[now<<1|1];
}
void pushdown(int now,int now_l,int now_r){
	if(tag[now] == -1)	return;
	int mid = (now_l + now_r)>>1;
	update(now<<1,now_l,mid,tag[now]);
	update(now<<1|1,mid+1,now_r,tag[now]);
	tag[now] = -1;
}
void modify(int now,int now_l,int now_r,int l,int r,int val){
	if(l > r)	return;
	if(l <= now_l && now_r <= r){
		update(now,now_l,now_r,val);
		return;
	}
	pushdown(now,now_l,now_r);
	int mid = (now_l + now_r)>>1;
	if(l <= mid)	modify(now<<1,now_l,mid,l,r,val);
	if(r > mid)		modify(now<<1|1,mid+1,now_r,l,r,val);
	pushup(now);
}
void build(int now,int now_l,int now_r){
	tag[now] = -1;
	if(now_l == now_r)	return;
	int mid = (now_l + now_r)>>1;
	build(now<<1,now_l,mid);build(now<<1|1,mid+1,now_r);
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;scanf("%lld",&n);
	for(int i=1; i<=n; i++)	scanf("%lld",&a[i]);
	stack<pair<int,int> > st;
	for(int i=1; i<=n; i++){
		lmx[i] = 0;
		while(!st.empty() && st.top().first < a[i])	st.pop();
		if(!st.empty())	lmx[i] = st.top().second;
		st.push({a[i],i});
	}
	while(!st.empty())	st.pop();
	for(int i=1; i<=n; i++){
		lmn[i] = 0;
		while(!st.empty() && st.top().first > a[i])	st.pop();
		if(!st.empty())	lmn[i] = st.top().second;
		st.push({a[i],i});
	}
	int ans = 0;
	build(1,1,n);
	for(int i=1; i<=n; i++){
		modify(1,1,n,lmx[i]+1,i-1,1);
		modify(1,1,n,lmn[i]+1,i-1,0);
		ans += sum[1];
	}
	printf("%lld\n",ans);
	return 0;
}

F.XOR Partition

题目描述:

小 C 认为,一个数集 $ S $ 的价值是这个数集中所有不同的数两两异或的最小值。数集大小不足 $ 2 $ 则价值记为 $ 2^{30} $。

小 C 又认为,一个数集 $ S $ 的一种分割方案的分割价值是这个数集分割成两个数集 \(A\)\(B\)\(A\)\(B\) 的价值的最小值。

求出使得分割价值取到最大的分割方案,如果多种输出任意一种。

$ 2 \le n \le 200000 , 0 \le a_i < 2^{30} $。

题目分析:

一个想法就是每次找到 \(a_i \oplus a_j\) 最小的数对,将这两个数分到不同的集合中,这样最后分出来就是最优解。
所以其实可以将最小异或的数对看成一条边,这样只要图为二分图就可以直接二分图染色。
那么只要图刚刚联通的时候,也就是为一棵树的时候就可以。

那么这个问题就是可以转化为一个类似瓶颈生成树的问题,也就是任意一条非树边权值均大于树边权值,其中 \((u,v)\) 的权值定义为 \(a_u \oplus a_v\),而所以直接求最小生成树就一定对应这样的一棵瓶颈生成树。

那么对于最小异或生成树该怎么求呢。
异或尽可能小相当于在 trie 树上其 lca 的深度尽可能大,而最可能大的就是一条链最底部的也就是同时拥有两个儿子的点。
而我们每插入一个点只会最多新增一个同时拥有两个儿子的点,所以符合条件的点数为 \(O(n)\)
就可以考虑枚举每一个符合条件的点,令子树大小最小的子树为左子树,然后枚举其左子树内的点,然后让它去和右子树匹配使得异或尽可能小。
这里可能就有问题就是如果点数达不到 \(n-1\) 也就是最后不连通怎么办,这种情况的出现就是因为出现了异或为 \(0\) 的情况,不会对答案产生任何影响。
分析一下复杂度,因为下一次枚举到某个点时,其对应子树的大小必然翻倍,而对于每一次枚举匹配的复杂度为 \(O(\log V)\),所以总时间复杂度为 \(O(n \log n \log V)\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e6+5;
const int INF = 1e9+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
struct node{
	int id,val;
}a[N];
int cnt,sz=1,id,head[N],col[N],ch[N][2],l[N],r[N];
bool vis[N];
bool cmp(node a,node b){
	return a.val < b.val;
}
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void insert(int now,int pos,int dep){
	if(dep == -1)	return;
	int s = (a[pos].val >> dep) & 1;
	if(ch[now][s])	r[ch[now][s]] = pos;
	else{
		ch[now][s] = ++sz;
		l[sz] = r[sz] = pos;
	}
	insert(ch[now][s],pos,dep-1);
}
int query(int now,int val,int dep){
	if(dep == -1){
		id = l[now];
		return 0;
	}
	int s = (val >> dep) & 1;
	if(ch[now][s])	return query(ch[now][s],val,dep-1);
	return query(ch[now][s^1],val,dep-1) + (1 << dep);
}
void solve(int now,int dep){
	if(dep == -1 || !now)	return;
	int x = ch[now][0],y = ch[now][1];
	if(x && y){
		if(r[x] - l[x] + 1 > r[y] - l[y] + 1)	swap(x,y); 
		int ans = INF,u = 0,v = 0;
		for(int i=l[x]; i<=r[x]; i++){
			int tmp = query(y,a[i].val,dep-1);
			if(ans > tmp)	ans = tmp,u = i,v = id;
		}
		add_edge(a[u].id,a[v].id);add_edge(a[v].id,a[u].id);
//		printf("%d %d\n",a[u].id,a[v].id);
	}
	solve(ch[now][0],dep-1);
	solve(ch[now][1],dep-1);
}
void dfs(int now,int val){
	col[now] = val;
	for(int i=head[now]; i; i=e[i].nxt){
		int to = e[i].to;
		if(col[to] != -1)	continue;
		dfs(to,val^1);
	}
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;scanf("%d",&n);
	for(int i=1; i<=n; i++)	scanf("%d",&a[i].val),a[i].id = i;
	sort(a+1,a+n+1,cmp);
	for(int i=1; i<=n; i++)	insert(1,i,30);
	for(int i=1; i<=n; i++)	col[i] = -1;
	solve(1,30);dfs(1,0);
	for(int i=1; i<=n; i++)	printf("%d",col[i]);
	return 0;
}