【题解】Educational Codeforces Round 152(CF1849)
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;
}