Codeforces Round 912 (Div. 2) - sol

发布时间 2023-12-04 19:16:35作者: H_W_Y

Codeforces Round 912 (Div. 2) - sol

Codeforces Round 912 (Div. 2)

一直是因为晚上打太晚了就没有打过 cf,所以只能 vp 了。/kk

四道题有关位运算——不好评价。

A. Halloumi Boxes

给出 \(n\) 个数 \(a_1,\dots,a_n\),和一个数 \(k\)

问是否能通过任意次翻转 \(\le k\) 的连续段使得 \(a\) 数组单调不减。

\(1 \le n \le 100\)

直接做就是了。

#include <bits/stdc++.h>
using namespace std;

const int N=105;
int T,n,k,lst,x;

void sol(){
  cin>>n>>k;bool fl=true;lst=0;
  for(int i=1;i<=n;i++){
  	cin>>x;
  	if(lst>x) fl=false;
  	lst=x;
  }
  if(fl) return cout<<"YES\n",void();
  cout<<((k==1)?"NO":"YES")<<'\n';
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>T;
  while(T--) sol();
  return 0;
}

B. StORage room

给出一个 \(n \times n\) 的矩阵 \(M\),需要构造一个长度为 \(n\) 的数组 \(a\),使得 \(\forall 1 \le i,j \le n,i \neq j\) 满足 \(a_i | a_j = M_{i,j}\)

\(1 \le n \le 10^3\)

简单推一下,

对于每一位,只要和有一个数或时这一位是 \(0\),就说明这个数的这一位为 \(0\)

反之我们钦定这一位是 \(1\) 一定不会劣。

最后判断一下构造出来是否合法即可。

#include <bits/stdc++.h>
using namespace std;

const int N=1e3+5;
int n,T,mp[N][N],a[N];
bool fl=true;

void sol(){
  cin>>n;
  for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cin>>mp[i][j];
  for(int i=1;i<=n;i++){
  	a[i]=0;
  	for(int k=0;k<30;k++){
  	  fl=true;
	  for(int j=1;j<=n;j++)
	    if(i!=j&&((mp[i][j]>>k)&1)==0) fl=false;
	  if(fl) a[i]|=(1<<k);	
	}
  }
  fl=true;
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
      if(i!=j&&(a[i]|a[j])!=mp[i][j]){fl=false;break;}
  if(!fl) return cout<<"NO\n",void();
  cout<<"YES\n";
  for(int i=1;i<=n;i++) cout<<a[i]<<' ';
  cout<<'\n';
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>T;
  while(T--) sol();
  return 0;
} 

C. Theofanis' Nightmare

给出一个长度为 \(n\) 的数组 \(a\),你需要把它分成若干个段,设段数为 \(k\)

\(\sum_{i=1}^{k} i \times sum_i\) 最大值,其中 \(sum_i\) 表示一段的和。

\(1 \le n \le 2 \times 10^5\)

居然被卡了一下,想了几分钟才知道的。

发现其实你只需要维护后缀,如果后缀 \(\gt 0\) 就加上贡献就可以了。

一个贪心的思路,主要来源于发现多分一个段的贡献是对于后面所有的,

这种思路有点类似于差分或者扫描线(最近 ds 做得有点多)。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=2e5+5;
int n,T;
ll a[N],ans=0;

void sol(){
  cin>>n;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=n-1;i>=1;i--) a[i]+=a[i+1];
  ans=a[1];
  for(int i=2;i<=n;i++) if(a[i]>0) ans+=a[i];
  cout<<ans<<'\n';
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>T;
  while(T--) sol();
  return 0;
}

D1. Maximum And Queries (easy version)

给定长度为 \(n\) 的序列 \(a\),定义一次操作是选择一个 \(i\),令 \(a_i \gets a_i + 1\)

\(q\) 次询问,每次给定 \(k\),求出最多进行 \(k\) 次操作后,整个序列按位与的最大值,即最大化

\[\operatorname{AND}_{i=1}^n a_i \]

\(1 \le n \cdot q \le 10^5\)

D1 就直接做就可以了,

枚举每一个位,只要可以就把它变成 \(1\) 即可。

#include <bits/stdc++.h>
using namespace std;
#define ll unsigned long long

const int N=1e6+5;
int n,m;
ll x,a[N],ans=0,b[N];

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=m;i++){
  	cin>>x;ans=0;
    for(int j=1;j<=n;j++) b[j]=a[j];
    for(int k=63;k>=0;k--){
      ll nw=0;
      for(int j=1;j<=n;j++){
      	if(((b[j]>>k)&1ll)) continue;
      	nw+=(1ll<<k)-b[j]+((b[j]>>k)<<k);
      	if(nw>x) break;
      }
      if(nw<=x){
      	ans+=(1ll<<k);x-=nw;
      	for(int j=1;j<=n;j++){
      	  if(((b[j]>>k)&1)) continue;
      	  b[j]=((b[j]>>k)<<k)+(1ll<<k);
      	}
      }
    }
    cout<<ans<<'\n';
  }
  return 0;
}

D2. Maximum And Queries (hard version)

\(1 \le n,q \le 10^6,0 \le a_i \le 10^6\)

和上一道题唯一的区别就在于数据范围。

同样容易想到去按位贪心,这一定是没错的。

那么现在问题就在于如何快速得计算到把这一位变成 \(1\) 的代价。

首先对于给出的 \(k\) 可以直接把 \(a_i\) 填到 \(2^{20}\) 次方的可以直接特判,

再来考虑余下来的 \(19\) 位。

发现一个重要的性质,假设当前枚举到第 \(i\) 位,前面的答案为 \(ans\)

如果当前的 \(a_i\) 中完全包含了 \(ans\),就是 \(ans\) 在二进制表达下为 \(1\) 的位置 \(a_i\) 也为 \(1\)

这些数的后面的位置是不会受到影响的。

而一旦一个数原本为 \(0\) 的位置被改成了 \(1\),那么这个数后面的位置就是 \(0\) 了。

那现在如何去维护呢?

有多少个数前面包含 \(ans\) 是好维护的,直接 dp 即可。

而对于本次的代价,我们还需要减去这次把 \(0\) 改成 \(1\) 的总和,

发现这也是好维护的,同样预处理出来。

于是就做完了,感觉看代码会更加清晰一些。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1048576;
int n,m,cnt,ans,f[20][N];
ll g[20][N],s,x;

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++){
  	cin>>x;s+=(1ll<<20)-x;
  	for(int j=0;j<20;j++) if(!((x>>j)&1)) f[j][x]++,g[j][x]+=x%(1ll<<j);
  }
  for(int j=0;j<20;++j)
    for(int k=0;k<20;++k)
      for(int i=0;i<(1<<20);++i)
        if(!((i>>k)&1)) f[j][i]+=f[j][i^(1<<k)],g[j][i]+=g[j][i^(1<<k)];
  while(m--){
  	cin>>x;
  	if(x>=s){cout<<((1ll<<20)+(x-s)/n)<<'\n';continue;}
  	ans=cnt=0;
  	for(int i=19;i>=0;i--){
  	  ll nw=(1ll*(cnt+f[i][ans])<<i)-g[i][ans];
  	  if(x>=nw) x-=nw,cnt+=f[i][ans],ans|=(1<<i);
  	}
  	cout<<ans<<'\n';
  }
  return 0;
}

E. Geo Game

这是一道交互题

在二维平面上有 \(n\) 个整点,开始有一个起点,先手玩家和后手玩家依次选点,与上轮对手选的点连条路径。如果这 \(n-1\) 条路径的平方和为偶数,则先手胜;否则后手胜。

你来选择先后,然后赢过对方。

\(1 \le n\le 10^5\)

发现这个东西至于每一次变化的奇偶性相关。

所以我们考虑 异或,记 \(v_i\) 表示对于第 \(i\) 个点的 \(x_i \oplus y_i\)

于是两个点连一条边,它对答案的奇偶性贡献就是 \(v_j \oplus v_i\)

这样一直写下去,发现在练出来的这条路径中,中间的点都会被异或两次得到 \(0\)

而对于答案的影响只是起点和终点的 \(v\)

起点是给定了的,所以我们能确定的也只是终点了。

发现如果 \(v_i=0\) 的个数较少,那么我们每一次一直选 \(v_i=0\) 的点是一定能选完的,

这样剩下来的终点就一定是 \(v_i = 1\) 的,于是胜负就可以提前判断以至于可以选出先后手了。

于是这道题就做完了,难点主要在于奇偶性到异或的转化。

具体实现的时候只需要判断一下和起点是否相同即可。

#include <bits/stdc++.h>
using namespace std;
#define pb push_back 

const int N=1e5+5;
int n,x,y,s,op=0,sx,sy;
vector<int> p[2];
bool vis[N];

void sol(){
  cin>>n>>sx>>sy;op=0;
  p[0].resize(0);p[1].resize(0);
  for(int i=1;i<=n;i++) cin>>x>>y,p[((x^sx)+(y^sy))%2].pb(i),vis[i]=false;
  if((int)p[0].size()>=(int)p[1].size()){
  	cout<<"First\n";cout.flush();
  	for(int i=1;i<=n;i++){
  	  if(i%2==0){cin>>x,vis[x]=true;continue;}
	  for(int j=1;j>=0;j--){
	  	while(p[j].size()&&vis[p[j].back()]) p[j].pop_back();
	  	if(p[j].size()){s=p[j].back(),p[j].pop_back();break;}
	  }
	  cout<<s<<'\n';cout.flush();
	}
  }
  else{
  	cout<<"Second\n";cout.flush();
  	for(int i=1;i<=n;i++){
  	  if(i%2==1){cin>>x,vis[x]=true;continue;}
	  for(int j=0;j<2;j++){
	  	while(p[j].size()&&vis[p[j].back()]) p[j].pop_back();
	  	if(p[j].size()){s=p[j].back(),p[j].pop_back();break;}
	  }
	  cout<<s<<'\n';cout.flush();
	}
  }
} 

int main(){
  int T;cin>>T;
  while(T--) sol();
  return 0;
}

F. Babysitting

给一个 \(n\) 个点 \(m\) 条边的无向图,每一个点有点的编号。

选出若干个点覆盖所有边且使得选出点的编号排序后相邻两点之差最小值最大。

一条边被覆盖当且仅当其一端被选中。

\(1 \le n \le 10^5,1 \le m \le 2 \times 10^5\)

超哥课上提了一下。

这道题还是比较套路的,首先两点中只用选一个就很容易想到是 2-SAT 问题,

而最小值最大就很容易想到去二分答案。

二分出的答案我们该如何去判断呢?

设当前二分出的答案为 \(x\)

那么对于每一个节点 \(i\),它是不能和 \([i-x+1,i-1]\)\([i+1,i+x-1]\) 中的元素同时选中的。

这样的思路很容易转化到图上面,和 2-SAT 的建图方式相似。

我们用编号 \(1 \sim n\) 表示要选第 \(i\) 个点,\(n+1 \sim 2n\) 表示不选 \(i\) 点。

于是对于 2-SAT ,设有边两端点为 \(u,v\),那么我们其实是建两条边:\(u+n \to v,v+n \to u\)

而对于二分答案的限制,我们相当于把 \(i\) 和那两个区间里面的 \(j+n\) 都建上一条边。

而这是一个点关于一个区间去建边,容易想到用线段树优化建图完成。

于是这道题就做完了,线段树优化建图+2-SAT。

注意二分上界的选取,只能取到 \(n\)

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int,int> 
#define fi first
#define se second

const int N=2e6+5;
int n,m,head[N],tot=0,idx=0,scc=0,bl[N],dfn[N],st[N],tp=0,low[N];
bool vis[N];
struct edge{int v,nxt;}e[N<<1];
pii a[N];

void add(int u,int v){e[++tot]=(edge){v,head[u]};head[u]=tot;}

void tarjan(int u){
  dfn[u]=low[u]=++idx;vis[u]=true;st[++tp]=u;
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(vis[v]) low[u]=min(low[u],dfn[v]);
  	else if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
  }
  if(dfn[u]==low[u]){
  	scc++;int x;
  	while((x=st[tp--])){
  	  bl[x]=scc;
  	  vis[x]=false;
  	  if(x==u) break;
  	}
  }
}

#define mid ((l+r)>>1)
#define lson l,mid,p<<1
#define rson mid+1,r,p<<1|1

int build(int l,int r,int p){
  if(l==r){add(n*2+p,l+n);return n*2+p;}
  add(n*2+p,build(lson));add(n*2+p,build(rson));
  return n*2+p;
}

void upd(int l,int r,int p,int x,int y,int v){
  if(x<=l&&y>=r) return add(v,n*2+p);
  if(x<=mid) upd(lson,x,y,v);
  if(y>mid) upd(rson,x,y,v);
}

#undef mid

bool chk(int x){
  tot=tp=idx=scc=0;
  for(int i=0;i<=6*n;i++) head[i]=dfn[i]=low[i]=vis[i]=bl[i]=0;
  build(1,n,1);
  for(int i=1;i<=m;i++) add(n+a[i].fi,a[i].se),add(n+a[i].se,a[i].fi);
  for(int i=1;i<=n;i++){
  	if(i>1) upd(1,n,1,max(1,i-x+1),i-1,i);
  	if(i<n) upd(1,n,1,i+1,min(n,i+x-1),i);
  }
  for(int i=1;i<=6*n;i++) if(!dfn[i]) tarjan(i);
  for(int i=1;i<=n;i++) if(bl[i]==bl[i+n]) return false;
  return true;
}

void sol(){
  cin>>n>>m;
  for(int i=1;i<=m;i++) cin>>a[i].fi>>a[i].se;
  int l=0,r=n;
  while(l<r){
  	int mid=(l+r+1)/2;
  	if(chk(mid)) l=mid;
  	else r=mid-1;
  }
  cout<<l<<'\n';
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int T;cin>>T;
  while(T--) sol();
  return 0;
}

Conclusion

  1. 对于有关奇偶性的问题我们可以考虑通过异或去表示。
  2. 预处理时不要只局限于 \(\mathcal O(n)\) 的预处理,带 \(\log\) 是完全没有问题的。