暑假集训Day19 比赛题解

2023-08-05 16:22:13

总结

这次打下来,由于 T2 贪心不够完全,T3 模拟 5 个时不是最优,T4 想到暴力做法但是来不及打,加之全都是捆绑测试点,导致我 T2,T3 虽然加起来有不少点对了,但是还是判全错,最后也只剩下 T1 的 100。

感觉这次前三题也不难,都是可做的,T4 的 30pts 暴力也很白给,但是我想加调了两个小时 T2 的假树形 dp,然后就来不及打了。

赛后发现 T2 的贪心和我想的基本一致,但是枚举的东西略有不同,也就是我的不够贪心。T3 的构造不难想,有一个挺显然的性质但是我并没有去在意。T1 有可以不用数位 dp 的更好方法,但我也没去找了。

总而言之,下次可以不需要这么心急,可以先打个暴力当对拍都不错,不过也挺好了,感觉自己的算法思维也越来越强了呢,还是有不错的提升的。

T4 的正解因为我还没有看懂,所以就不放了,等我自己想明白了再来搞。

那还是先进入正题吧~

A

题目描述

n+m 堆石子,n 堆中第 i 堆有 i 个石子,m 堆中第 i 堆中有 ai 堆石子,问你进行 nim 游戏时先手第一步有多少种取法能保证自己获胜。(第一步之后均采用最优策略)。

1n1018,1m105,1ai1018

思路

sum=i=1ni i=1mai(即所有元素的亦或和)。

因为我上课的时候证明 nim 游戏的正确性的时候没有认真听,所以一开始以为我从大于等于 sum 的数中取 sum 个即可。但是显然用脑子想想都发现不对,我甚至还因为这个去问出题人要样例的解释,后来自己模拟一遍才发现不对劲,完全不是一个东西。

思考正解,设我们选了取个数为 x 的一堆,假设取后是 x 那么 sum=sumxx,因为要保证必胜,学过 nim 的都知道最后要保证 sum=0,所以我们也可以得到: sumx=x。因为是取后,所以 x<x,即 sumx<x

所以最终只需要求 sumx<x 的数的个数即可。

对于 m105,我们直接枚举 ai 即可,但是 n1018,总不能一个一个枚举吧?

此时的瓶颈有两个了,第一个是 n 堆的异或和不好求,第二个是答案也不好求。

首先解决异或的问题,我们通过打表可以得到一个结论,当 nmod4=3 时,n 的前缀异或和为 0

证明也很简单:

我们设 n4 的倍数,那么二进制下:

n=?????00

n+1=?????01

n+2=?????10

n+3=?????11

显然 n(n+1)(n+2)(n+3)=0,得证。

sum 最高位是第 k 位。

然后再通过分类讨论可以得到只有当 x 的第 k 位为 1 时满足条件。

所以机智的我懒得思考有的没的,直接现场 3 分钟打了一个数位 dp。

Code

ll n,m,sum,k,a[100005],f[2][2][102],A[102];
ll dfs(ll len,ll lim,ll isk){
if(~f[lim][isk][len])return f[lim][isk][len];
if(!len)return f[lim][isk][len]=isk;
ll maxx=lim?A[len]:1,res=0;
for(ll i=0;i<=maxx;i++){
if(len==k+1)res+=dfs(len-1,lim&(i==maxx),i==1);
else res+=dfs(len-1,lim&(i==maxx),isk);
}
return f[lim][isk][len]=res;
}
ll solve(ll x){
memset(f,-1,sizeof(f));
ll cnt=0;
while(x){
A[++cnt]=x&1;
x>>=1;
}
return dfs(cnt,1,0);
}
int main(){
n=read(),m=read();
for(ll i=1;i<=m;i++){
a[i]=read();
sum^=a[i];
}
if(n%4==0)sum^=n;
else if(n%4==1)sum^=(n-1)^n;
else if(n%4==2)sum^=(n-2)^(n-1)^n;
if(!sum)puts("0");
else{
ll ans=0;
k=__lg(sum);
for(ll i=1;i<=m;i++){
if(a[i]&(1ll<<k))ans++;
}
ans+=solve(n);
cout<<ans;
}
}

1n 中的答案其实是可以推式子的,我们把二进制的 n 分成 [1,k1],[k+1,kn]两个部分,当 n 的第 k1 时,答案就是两部分的总方案数相乘,如果为 0,那也只有 [k+1,kn] 为原部分的方案数不行。

核心代码

ll ans=0;
k=__lg(sum);
for(ll i=1;i<=m;i++){
if(a[i]&(1ll<<k))ans++;
}
ans+=(n>>k+1ll)*(1ll<<k);//先把不含自己的方案数算上
ans+=max(n%(1ll<<k+1ll)-(1ll<<k)+1ll,0ll);//再加上如果 k 位有1的方案数
cout<<ans;

总得来说还是简单的,但是因为好不容易找出正解了考场上打出来也是挺开心的(打的比较细心,基本上就微调过大样例),后来发现原来不需要这样麻烦,甚至有些神仙通过打表找规律找到正解,悲伤。(郑神!!!%%%%)

B

问题描述

给你一棵树,每个节点得染成红色或者蓝色,i 点染成红色的代价为 ai,染成蓝色为 bi,最后必须保证对于每个点,它的子树内的点(含己)上面的红点总数和蓝点总数的差的绝对值不超过 1,求最小代价。

3n106

思路

一眼树形 dp,但是不知道具体怎么做,开始是想把每个儿子节点看成物品然后跑背包的,奈何本人技术力不够打不来这种背包,然后就没想着去拿这 O(n2)40 分。

还是看限制条件吧,“对于任意一个点,子树内”,那我们不难发现对于每个子树只有三种情况:R=B,R=B+1,R=B1R 表示子树内红色点个数,B 表示子树内红色点个数)。

并且,我们观察到如果一个子树的大小是偶数,那只可能出现蓝色等于红色的情况,这种情况因为蓝色等于红色,怎么加都不会不合法,所以保证代价最小即可,不用理会。

而当大小为奇数时,有两种情况。

所以我们设计状态 f[0(R=B)/1(R=B+1)/2(R=B1)][u] 表示以 u 为根节点的子树,红蓝配比为 (R=B)/(R=B+1)/(R=B1) 的最小代价,仔细思考不难发现转移:

f[0][u]=min(ansR=B+1+b[u],ansR=B1+a[u])f[1][u]=min(ansR=B+2+b[u],ansR=B+a[u])f[2][u]=min(ansR=B2+a[u],ansR=B+b[u])

其中 anscase 表示没加入根时子树的颜色情况为 case 时的最小代价。

anscase 答案怎么求呢?首先如果儿子的子树大小为偶,直接加入贡献即可,因为此时加入不会对颜色的情况造成影响,然后对奇数大小的儿子贪心取最优即可,贡献可以表示为:

ansRB=x=vson[]uf[0][v]+min{i=1,vson[]ukrf[1][v]+i=1,vson[]ukbf[2][v]}

其中 kr+kb=sum,krkb=x

怎么贪心取最优?我们假设先全选蓝色,然后把红色减蓝色的贡献加入优先队列,取最小的即可。

Code

int n; // 0:R=B 1:R>B 2:R<B
ll a[1000006],b[1000006],f[3][1000006],siz[1000006];
vector<int>F[1000006];
void dfs(int x,int fa){
priority_queue<ll,vector<ll>,greater<ll> >Q;
siz[x]=1;
ll ans=0;
for(int y:F[x]){
if(y==fa)continue;
dfs(y,x);
siz[x]+=siz[y];
if(siz[y]&1){
ans+=f[2][y];
Q.push(f[1][y]-f[2][y]);
}else ans+=f[0][y];
}
if(!Q.size()){
f[1][x]=ans+a[x],f[2][x]=ans+b[x];
return;
}
if(siz[x]&1){
//奇数的时候,一定有偶数个子树不是偶siz
//要么是少两个b然后选b,要么少两个a选a,要么不多不少选a或者选b
//两种答案,各分两种情况讨论。
int cnt=Q.size()>>1;//枚举选几个a,不减1是持平的
cnt--;
while(cnt--){
ans+=Q.top(),Q.pop();
}
f[2][x]=min(ans+a[x],ans+Q.top()+b[x]);
ans+=Q.top();
Q.pop();
f[1][x]=min(ans+a[x],ans+Q.top()+b[x]);
}else{
//偶数的时候,一定有奇数个子树不是偶siz
//要么是少a选b,要么是少b选a
int cnt=Q.size()>>1;//枚举选几个a,这里是先少选a的
while(cnt--){
ans+=Q.top(),Q.pop();
}
f[0][x]=min(ans+a[x],ans+Q.top()+b[x]);
}
}
int main(){
n=read();
for(int i=1;i<=n;i++){
a[i]=read(),b[i]=read();
}
for(int i=1;i<n;i++){
int u=read(),v=read();
F[u].push_back(v);
F[v].push_back(u);
}
dfs(1,0);
ll ans;
if(n&1)ans=min(f[1][1],f[2][1]);
else ans=f[0][1];
cout<<ans;
}

一开始其实思路跟这个差不多,但是由于我不想开三维,就开了一维然后分奇偶讨论,天真地以为奇数大小的子树中除根的节点必然满足红色等于蓝色,然后方法差不多,直接把奇数情况的 a[v]b[v] 扔进优先队列了,没想到其实与根的颜色关系不大,要看的是子树的颜色,然后就全 WA 了(邪恶的捆绑测试点)。

下次讨论 dp 的时候不要想当然,要关注整体的贡献而不是个体的贡献(根节点的颜色不影响整体,看得是子树多的颜色),多思考,反思,看题也要仔细(一开始以为不算根)!!

C

分类讨论大挂了,不过问题不大,其实也想到了一些构造思想,但是没想到那么显然。

题意:

给定一个 n 个点的完全图,你要给每条边定一个方向,使得得到的有向完全图(又称竞赛图)的三元环个数最多。(n1000)

思路

观察图中任意三个点的关系,发现要么是三元环,要么就是一个点指向另外两个点。三元环的个数不好算,但是第二个的个数好算,我们令 i 点的出度为 degi,那么非三元环的个数就是 i=1n(degi2),三元环的个数即为:(n3)i=1n(degi2)。而:

i=1n(degi2)=12i=1n(degi1)degi=12i=1ndegi212i=1ndegi=12i=1ndegi2n(n1)4

所以只需要最小化 i=1ndegi2 即可,观察到一个点的出度与入度越接近,这个值就越小。

提供两个构造方法:

  • 奇数个点时显然每个点可以做到有 n12 个出度,然后偶数个只需要在奇数个的基础上加一个点平摊出度即可

Code1

m=n;
if(n+1&1)m--;
for(int i=1;i<=m;i++){
for(int j=1;j<=m;j++){
if(i!=j&&((m-i+j)<=(m-1>>1)||(j-i<=(m-1>>1)&&j>i)))
a[i][j]=1;
}
}
if(n+1&1){
for(int i=1;i<=(m>>1);i++)a[i][n]=1;
for(int i=(m>>1)+1;i<=m;i++)a[n][i]=1;
}
for(int i=1;i<=n;i++,puts("")){
for(int j=1;j<=n;j++){
printf("%d",a[i][j]);
}
}
  • 就是隔一个连一个,这样基本可以平摊出度。

Code2

int n=read();
for(int i = 1;i<=n;i++) for(int j = i+1,t=1;j<=n;j++) a[i][j]=t,t=1-t;
for(int i = 1;i<=n;i++) for(int j = i+1,t=0;j<=n;j++) a[j][i]=t,t=1-t;
for(int i = 1;i<=n;i++){for(int j = 1;j<=n;j++)cout<<a[i][j];cout<<"\n";}

然后我目前听得懂的题目就这么多了,把 D 放出来让大佬们思考一下。

D

问题描述

给定一个 n 个点的树 G,称它的一个子图 G 为“子树”当且仅当 G=G,或者存在 G 上的一条边,将 G 断掉这条边之后 G 是分成的两个连通块之一。

对于所有的 k=1,2,3n,求从这棵树中等概率选出 k 个不同的点,包含它们的最小的“子树”的大小的期望模 998244353 的值。

3n7000

样例

in:

5
1 2
1 3
2 4
2 5

out

798595484
499122180
4
199648875
5

思路

我只想到了 n15 时,枚举断掉的边和选中的点,然后搜索即可。

本文作者:NBest

本文链接:https://www.cnblogs.com/NBest/p/17686910.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   NBest  阅读(4)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起