A. Buttons
题意:
A
l
i
c
e
Alice
Alice 和
B
o
b
Bob
Bob 在玩一个游戏,有三种按钮,其中
a
a
a 按钮只能被
A
l
i
c
e
Alice
Alice 按,
b
b
b 按钮只能被
B
o
b
Bob
Bob 按
c
c
c 按钮两个人都可以按,每个按钮只能被按一次,两个人轮流按,每次只能按一个
如果轮到这个人按,但是他没有可以按的按钮了,那么他就输
判断谁是赢家
思路:
两个人肯定优先按
c
c
c 类按钮,然后才按自己的那一类按钮,判断一下数量就可以
// Problem: Buttons
// Contest: Codeforces
// URL: https://m1.codeforces.com/contest/1858/problem/A
// Memory Limit: 256 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--){
ll a,b,c;
std::cin>>a>>b>>c;
ll x1=a+(c&1); //如果c是奇数 A比B多拿一个c 否则两人那的c按钮一样多
ll x2=b;
std::cout<<(x1>x2?"First":"Second")<<endl;
}
return 0;
}
B. The Walkway
题意:
有
n
n
n 个海滩,
m
m
m 个售卖店分布在其中一些海滩,第
i
i
i 个售卖店在第
s
i
s_i
si 个海滩
P
e
t
y
a
Petya
Petya 从第一个海滩按顺序走向最后一个海滩,如果至少符合以下一个条件,
P
e
t
y
a
Petya
Petya 会吃一块饼干:
- 在第 i i i 个海滩有一个售卖店,他会买一块饼干并吃掉
- 他在第一个海滩会直接吃掉一块饼干
- 距离上次吃饼干的海滩到现在,已经有
d
d
d 个海滩没吃饼干了,他会在这个海滩吃下一块。
例如 d = 2 d=2 d=2 ,上次在 2 e d 2ed 2ed 海滩吃了饼干, 3 3 3 号没吃,那么在 4 4 4 号会吃下一块饼干
P
e
t
y
a
Petya
Petya 在每个海滩最多吃下一个饼干,现在要移走其中一个售卖店,要求最后他吃的饼干最少
求出他最后最少吃多少块饼干,并给出相应的移走售卖店的方案数
思路:
题意比较复杂,但是其实就是简单预处理一下一个售卖店都不移走的话,能吃多少块饼干
因为假如移走的是第
i
i
i 号售卖店,它只会影响
i
−
1
i-1
i−1 到
i
+
1
i+1
i+1 号售卖店这段区间吃的饼干
其余区间吃饼干的活动并不会被干扰!
因此我们可以直接枚举这个移走的店就可以,同时统计一下方案数
要注意一些edge case例如第一间店在 1 1 1 或者最后一间店在 n n n
// Problem: The Walkway
// Contest: Codeforces
// URL: https://m1.codeforces.com/contest/1858/problem/B
// Memory Limit: 256 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--){
int n,m,d;
std::cin>>n>>m>>d;
std::vector<int> a(m+5); //售卖店坐标
a[0]=1;
a[m+1]=n;
fore(i,1,m+1) std::cin>>a[i];
int ans=INF,cnt=1; //吃掉的饼干数和方案数
int x=1; //在1号吃的饼干
if(a[1]==1) x=0; //如果1号就有一个店,就撤销这个,后面算每个店吃的饼干再加上去
fore(i,1,m+1){ //预处理一个店都不移走吃的饼干
++x;
x+=(a[i]-a[i-1]-1)/d;
}
x+=(n-a[m])/d; //最后一个店到n点这一段
fore(i,1,m+1){
int res=0;
if(i==1){
res+=x-1;
if(a[1]>1) res-=(a[1]-2)/d;
else ++res;
res-=(a[i+1]-a[i]-1)/d;
res+=(a[i+1]-2)/d;
}
else if(i==m){
res+=x-1;
res-=(a[i]-a[i-1]-1)/d;
if(a[i]<n) res-=(n-a[i])/d;
res+=(n-a[i-1])/d;
}
else res=x-1-(a[i+1]-a[i]-1)/d-(a[i]-a[i-1]-1)/d+(a[i+1]-a[i-1]-1)/d;
if(res<ans){
ans=res;
cnt=1;
}
else if(res==ans) ++cnt;
}
std::cout<<ans<<' '<<cnt<<endl;
}
return 0;
}
C. Yet Another Permutation Problem
题意:
对于一个长度为
n
n
n 的排列
a
1
,
a
2
.
.
.
a
n
a_1,a_2...a_n
a1,a2...an,定义
d
i
=
g
c
d
(
a
i
,
a
(
(
i
m
o
d
n
)
+
1
)
d_i = gcd(a_i,a_{((i\hspace{2pt}mod\hspace{2pt}n)+1})
di=gcd(ai,a((imodn)+1)
定义
s
c
o
r
e
score
score :
d
i
d_i
di 里面不同数的个数
给出 s c o r e score score 最高的长度为 n n n 的排列
思路:
首先,
d
i
d_i
di 一定
≤
⌊
n
2
⌋
\leq \lfloor \dfrac{n}{2} \rfloor
≤⌊2n⌋ ,因此对于一个长度为
n
n
n 的排列,最大的
s
c
o
r
e
score
score 就是
⌊
n
2
⌋
\lfloor \dfrac{n}{2} \rfloor
⌊2n⌋ ,
d
i
∈
[
1
,
⌊
n
2
⌋
]
d_i \in [1,\lfloor \dfrac{n}{2} \rfloor]
di∈[1,⌊2n⌋]
只需要对每个 d i d_i di 后面跟一个 2 d i 2d_i 2di 就可以构造出 d i = g c d ( d i , 2 d i ) d_i = gcd(d_i , 2d_i) di=gcd(di,2di)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--){
int n;
std::cin>>n;
for(int i=1;i<=n;i+=2)
for(int j=i;j<=n;j<<=1)
std::cout<<j<<' ';
std::cout<<endl;
}
return 0;
}
D. Trees and Segments
题意:
给定一个长度为
n
n
n 的
01
01
01 串
s
s
s,定义
l
0
、
l
r
l_0、l_r
l0、lr:
- l 0 l_0 l0 是最大连续 0 0 0 的数量
- l 1 l_1 l1 是最大连续 1 1 1 的数量
对于一个正整数 a a a ,定义 b e a u t y beauty beauty :
- b e a u t y = a ⋅ l 0 + l 1 beauty = a \cdot l_0 + l_1 beauty=a⋅l0+l1
你最多可以修改 k k k 次 s s s 中的字符,求出 a ∈ [ 1 , n ] a \in [1,n] a∈[1,n] 各自对应的最大 b e a u t y beauty beauty
思路:
如果对于每一个
a
a
a ,我们枚举
l
1
l_1
l1 的长度,可以得到这个
l
1
l_1
l1 对应的
l
0
l_0
l0 的话,就是这个
a
a
a 的答案
这里时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)
问题就转化为:求
l
1
∈
[
1
,
n
]
l_1 \in [1,n]
l1∈[1,n] 对应的最大
l
0
l_0
l0
不难发现:对于一段长度为
l
1
l_1
l1 的连续
1
1
1 区间,两边肯定都是
0
0
0 ,否则
l
1
l_1
l1 肯定会更大
因此
l
0
l_0
l0 一定在
l
1
l_1
l1 的左边或者右边,问题是如何快速求出这个
l
0
l_0
l0 ?
考虑
D
P
DP
DP
定义
p
r
e
[
i
]
[
j
]
pre[i][j]
pre[i][j] 为前缀
1
→
i
1\rightarrow i
1→i 最多 修改
j
j
j 次 并且 最长
0
0
0 串 在
i
i
i 结束的最大
l
0
l_0
l0,那么它的转移方程可以写成这样:
p r e [ i ] [ j ] = { p r e [ i − 1 ] [ j ] + 1 , i f : s [ i ] = 0 p r e [ i − 1 ] [ j − 1 ] + 1 , i f : s [ i ] = 1 a n d j > 0 0 , o t h e r w i s e pre[i][j]= \begin{cases} pre[i-1][j] + 1 \quad, if: \quad s[i]=0 \\\\ pre[i-1][j-1] + 1 \quad, if: \quad s[i]=1 \quad and \quad j>0\\\\ 0, \quad otherwise \end{cases} pre[i][j]=⎩ ⎨ ⎧pre[i−1][j]+1,if:s[i]=0pre[i−1][j−1]+1,if:s[i]=1andj>00,otherwise
第一种情况是直接把 0 0 0 拼接上去,第二种情况是把 1 1 1 改成 0 0 0,第三种情况就是没有操作数了
这样子得出来的局限是 必须在
i
i
i 位置结束 ,我们可以将前缀向后传递,取
m
a
x
max
max 值
这样就可以将
p
r
e
[
i
]
[
j
]
pre[i][j]
pre[i][j] 转化为前缀
1
→
i
1\rightarrow i
1→i 区间最多操作
j
j
j 次的最大
l
0
l_0
l0 ,且不限制结束位置
类似地可以定义 s u f suf suf 后缀,这样就可以对于一段长度为 l 1 l_1 l1 的区间快速求出两边的 m a x l 0 max \hspace{2pt}l_0 maxl0
枚举连续 1 1 1 区间,算出里面原来 0 0 0 的个数 x x x,把 0 0 0 全部转化成 1 1 1 ,然后求出剩余操作数 k − x k-x k−x 可以在两边得到的最大 l 0 l_0 l0,记录一下即可
注意要算 l 1 = 0 l_1 = 0 l1=0 的情况
// Problem: D. Trees and Segments
// Contest: Codeforces - Codeforces Round 893 (Div. 2)
// URL: https://codeforces.com/contest/1858/problem/D
// Memory Limit: 256 MB
// Time Limit: 3000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
void solve(){
int n,k;
std::cin>>n>>k;
std::string s;
std::cin>>s;
s='0'+s; //让下标从1开始
std::vector<std::vector<ll>> pre(n+2,std::vector<ll>(n+2,0));
std::vector<std::vector<ll>> suf(n+2,std::vector<ll>(n+2,0));
std::vector<ll> ans(n+1,0);
std::vector<ll> dp(n+1,-1000000); //连续'1'的长度为i时,对应的最大'0'长度
fore(i,1,n+1) //计算前缀
fore(j,0,k+1){
if(s[i]=='0') pre[i][j]=std::min(pre[i-1][j]+1,1ll*i);
else if(s[i]=='1' && j>0) pre[i][j]=std::min(pre[i-1][j-1]+1,1ll*i);
else pre[i][j]=0;
}
for(int i=n;i>=1;--i) //计算后缀
fore(j,0,k+1){
if(s[i]=='0') suf[i][j]=std::min(suf[i+1][j]+1,1ll*n-i+1);
else if(s[i]=='1' && j>0) suf[i][j]=std::min(suf[i+1][j-1]+1,1ll*n-i+1);
else suf[i][j]=0;
}
fore(i,1,n+1) //前缀向后传递
fore(j,0,k+1){
if(i>0) pre[i][j]=std::max(pre[i][j],pre[i-1][j]);
if(j>0) pre[i][j]=std::max(pre[i][j],pre[i][j-1]);
}
for(int i=n;i>=1;--i) //后缀向前传递
fore(j,0,k+1){
if(i<n) suf[i][j]=std::max(suf[i][j],suf[i+1][j]);
if(j>0) suf[i][j]=std::max(suf[i][j],suf[i][j-1]);
}
fore(l,1,n+1){
int x=0; //把这段区间全部变为'1' 需要的操作数
fore(r,l,n+1){
x+=(s[r]=='0');
if(k<x) break;
int len=r-l+1;
dp[len]=std::max({dp[len],pre[l-1][k-x],suf[r+1][k-x]});
}
dp[0]=std::max({dp[0],pre[l][k],suf[l][k]}); //L1=0的情况
}
fore(a,1,n+1)
fore(len,0,n+1){ //枚举 L1
ans[a]=std::max(ans[a],len+1ll*a*dp[len]);
}
fore(i,1,n+1) std::cout<<ans[i]<<" \n"[i==n];
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--){
solve();
}
return 0;
}
E1/E2. Rollbacks
这两题 e a s y v e r s i o n easy\hspace{2pt}version easyversion 和 h a r d v e r s i o n hard\hspace{2pt}version hardversion 的区别就是 E 1 E1 E1 可以离线,而 E 2 E2 E2 强制在线
题意:
数组
a
a
a 初始为空,有四种操作:
- + x +\quad x +x,将 x x x 添加到 a a a 的末尾
- − k - \quad k −k,移除 a a a 的最后 k k k 个数
- ! ! ! ,撤销上一个操作一或操作二
- ? ? ?,输出现在 a a a 里面有多少个不同的数
思路:
这题的做法是学习借鉴 jiangly 的代码和思路,有
q
l
o
g
q
qlogq
qlogq 的做法也有线性的做法
先考虑操作1和操作2,我们定义 p o s [ i ] pos[i] pos[i] 为 i i i 第一次出现的下标,如果是第一次出现,那么从这个位置开始往后的每一位,答案都要 + 1 +1 +1,最后询问答案就是 t r e e [ n ] tree[n] tree[n] 的值, n n n 就是现在数组 a a a 里面的元素个数
考虑树状数组来完成这个单点修改,区间查询的操作
如果是第一次出现,从这个位置开始一直到
t
r
e
e
tree
tree 数组的末尾,全部
+
1
+1
+1
操作二的话直接
n
−
=
k
n-=k
n−=k 就可以了,因为这样就又回到了
k
k
k 个数之前的
t
r
e
e
tree
tree
现在考虑操作三的撤销,如果是撤销操作二的话,直接
n
+
=
k
n+=k
n+=k 就可以,因为最后一次操作是操作二,意味着
t
r
e
e
tree
tree 数组没有被修改
如果是撤销操作一 的话,要考虑数组
a
a
a 现在这个位置的值
x
x
x ,和这个位置之前的值
z
z
z
因为操作一添加一个数
x
x
x 的话,这个位置可能以前就有添加过一个数
z
z
z ,
z
z
z 可能在
t
r
e
e
tree
tree 留下了它第一次出现的贡献,所以要先减去
z
z
z 的贡献,然后再考虑
x
x
x 是否第一次出现,是否需要区间修改
基于这个准则,我们撤销操作一,也应该要把现在的数 x x x 的贡献看看要不要去掉(如果它是第一次出现的话),还要把之前的数 z z z 的贡献给加上(如果 z z z 是在这个位置第一次出现的话)那么怎么判断这两个数是否第一次出现?
jiangly 的做法非常天才,他先把这个要添加的新的 x x x 以及他现在的第一次出现的位置 p o s [ x ] pos[x] pos[x] 、还有这个位置之前的数 z z z 以及 p o s [ z ] pos[z] pos[z] 插入到一个 o p t opt opt 数组中,用来记录所有的操作一和操作二,方便后面撤销
注意这个时候插入
o
p
t
opt
opt 的
p
o
s
[
x
]
pos[x]
pos[x] 和
p
o
s
[
z
]
pos[z]
pos[z] 都是旧值,对于他们的更新是插入完后才更新的
如果
p
o
s
[
z
]
=
=
n
pos[z]==n
pos[z]==n ,也就是说
n
n
n 这个位置是
z
z
z 的第一次出现的位置,现在把它的贡献删掉,并把
p
o
s
[
z
]
pos[z]
pos[z] 设置为无穷大
(
q
+
1
)
(q+1)
(q+1) ,同理,如果
p
o
s
[
x
]
>
n
pos[x]>n
pos[x]>n ,说明现在数组里
x
x
x 是第一次出现,要把
x
x
x 在之前位置的贡献删掉,在
n
n
n 这个位置加上新贡献,并把
p
o
s
[
x
]
pos[x]
pos[x] 设置为
n
n
n
看到这里,不难想到,撤销时,如果发现一个数 x x x 的旧的 p o s pos pos 值和现在的 p o s pos pos 值不一样的话,这个数的贡献一定被修改过了,现在要撤销这个修改就很容易了,直接把现在的贡献减掉,把之前的贡献加回去就可以了
对于这个位置旧的值 z z z 也是同样的道理
树状数组做法:
时间复杂度:
O
(
q
l
o
g
q
)
O(qlogq)
O(qlogq)
// Problem: E1. Rollbacks (Easy Version)
// Contest: Codeforces - Codeforces Round 893 (Div. 2)
// URL: https://codeforces.com/contest/1858/problem/E1
// Memory Limit: 256 MB
// Time Limit: 3000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
#define lowbit(x) ((x)&-(x))
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
const int N=1000050;
int a[N]; //a数组及里面的数字
struct Fenwick{
int n;
std::vector<int> tree;
Fenwick(int n){
this->n=n;
tree.assign(n,0);
}
void update(int x,int d){
while(x<n){
tree[x]+=d;
x+=lowbit(x);
}
}
int sum(int x){
int res=0;
while(x>0){
res+=tree[x];
x-=lowbit(x);
}
return res;
}
};
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int q;
int n=0; //不初始化喜提两发RE
std::cin>>q;
Fenwick fen(q+5);
std::vector<std::array<int,4>> opt;
std::vector<int> pos(N,q+1);
fore(i,1,q+1){
char c;
std::cin>>c;
if(c=='+'){
int x;
std::cin>>x;
++n;
opt.push_back({x,pos[x],a[n],pos[a[n]]}); //先把旧值保留
if(pos[a[n]]==n){ //之前残留的元素刚好在这个位置第一次出现
fen.update(n,-1); //要维护后面的tree
pos[a[n]]=q+1;
fen.update(q+1,1);
}
if(pos[x]>n){ //fist appearance
fen.update(pos[x],-1);
pos[x]=n;
fen.update(n,1);
}
a[n]=x;
}
else if(c=='-'){
int k;
std::cin>>k;
n-=k;
opt.push_back({-1,k});
}
else if(c=='?'){
std::cout<<fen.sum(n)<<endl;
//std::cout.flush(); E2要加上这一句来刷新,不然读入不了后面的操作
}
else{
auto [x,y,z,w]=opt.back();
opt.pop_back();
if(x==-1) n+=y; //'-'操作复原
else{
if(y!=pos[x]){ //y是旧值 如果跟现在的pos不一样,一定是修改过
fen.update(pos[x],-1);
pos[x]=y;
fen.update(y,1);
}
if(w!=pos[z]){ //w是旧值
fen.update(pos[z],-1);
pos[z]=w;
fen.update(w,1);
}
a[n]=z;
--n;
}
}
}
return 0;
}
赛后 jiangly 又指出这题可以使用线性做法过掉
其实就是把树状数组的单点修改改成了差分和前缀和,用这两个来维护答案
因为假如现在加到了
n
n
n 个数,询问的话也最多问到
n
n
n,不会询问到
n
n
n 后面的答案
所以我们可以将树状数组的单点修改,改成在差分数组上的修改,后面继续添加新的数字的时候,加上差分数组记录的贡献就可以
但是这种做法要注意先加了几个数,然后删掉
k
k
k 个数后,又加了几个数,最后一直撤销到删除
k
k
k 个数之前,
h
a
c
k
hack
hack 数据 (输出应该是
3
3
3):
10
+ 1
+ 2
+ 3
- 2
+ 1
+ 2
!
!
!
?
这种操作方法的话,最后询问的
s
u
n
[
n
]
sun[n]
sun[n] 可能已经被修改了,而且
s
u
m
[
n
−
1
]
sum[n-1]
sum[n−1] 很有可能在删除了
k
k
k 个数之后,加上新的一些数的时候被更新了,因此现在撤销的话不能直接
s
u
m
[
n
]
=
s
u
m
[
n
−
1
]
+
d
[
n
]
sum[n] = sum[n-1] +d[n]
sum[n]=sum[n−1]+d[n] 来算
必须在添加新的数的时候把旧的
s
u
m
sum
sum 值插入到
o
p
t
opt
opt 数组中,撤销的时候直接赋值,这样就可以保证正确性
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
const int N=1000050;
int a[N];
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int q;
std::cin>>q;
std::vector<int> pos(N,q+1);
std::vector<std::array<int,5>> opt;
std::vector<int> sum(q+1,0); //前缀和
std::vector<int> d(q+5,0); //差分数组
int n=0;
fore(i,1,q+1){
char c;
std::cin>>c;
if(c=='+'){
int x;
std::cin>>x;
++n;
opt.push_back({x,pos[x],a[n],pos[a[n]],sum[n]});
if(pos[a[n]]==n){
--d[n];
pos[a[n]]=q+1;
++d[q+1];
}
if(pos[x]>n){
--d[pos[x]];
pos[x]=n;
++d[n];
}
sum[n]=sum[n-1]+d[n];
a[n]=x;
}
else if(c=='-'){
int k;
std::cin>>k;
n-=k;
opt.push_back({-1,k});
}
else if(c=='?'){
std::cout<<sum[n]<<endl;
std::cout.flush();
}
else{
auto [x,y,z,w,t]=opt.back();
opt.pop_back();
if(x==-1) n+=y;
else{
sum[n]=t; //必须储存旧值 因为sum[n-1]可能已经改变,不是旧的sum[n-1]
if(y!=pos[x]){
--d[pos[x]];
pos[x]=y;
++d[y];
}
if(w!=pos[z]){
--d[pos[z]];
pos[z]=w;
++d[w];
}
a[n--]=z;
}
}
}
return 0;
}