2022 China Collegiate Programming Contest (CCPC) Guangzhou Onsite M. XOR Sum(数位dp 数位背包)

题目

给定n,m,k(0<=n<=1e15,0<=m<=1e12,1<=k<=18),

求长度为k的数组a,ai为[0,m]的整数,

满足\sum_{i=1}^{k}\sum_{j=1}^{i-1}a_{i}\bigoplus a_{j}=n的方案数

答案对1e9+7取模

题解

第一反应想起了hdu3693,但比对了一下,感觉那个题难很多,

两年前写的题忘了写题解,现在不会了

Educational Codeforces Round 104 (Rated for Div. 2) F.Ones(数位dp)_Code92007的博客-CSDN博客

后来想了想,感觉更像这个题,按余数统计方案数,

控制余数在一定范围,姑且称它为数位背包

每一位的贡献可以独立考虑,而这个式子实际是(i,j)对的01值不同就会产生贡献,

所以只用关注当前几个填1几个填0,由于有的卡上界有的不卡,所以记录一下当前有几个卡上界

dp[i][j][l]表示当前从高到低考虑到低i位时,只考虑n的i位及更高位时的数的当前余数是j,选的l个数卡上界的方案数

k=18时,最大贡献是9*9=81,考虑当前n的余数x最大能是多少,

(x-81)*2<x,表示本位取了81之后,下一位的余数小于81,解得x<162,

所以转移过程中可以忽略>162的转移,因为通过更低的位,再也不能使其归0

而<0的转移也不可取,

因为这个操作,转移到下一位,相当于*2,下一位n有余数,相当于+1,

还有减去当前选的值,相当于减去一个非负数,而(-1)*2+1还为-1,最终不能变为0

所以,可以只关注[0,162)的转移,代码中写的是[0,256)

此外,初始局面时,n的余数超过256也显然无解

转移分两种情况讨论,当前位为0 或者 当前位为1

1. 当前位为0,卡上界的只能选0,for枚举不卡上界的数中选了x个1

2. 当前位为1,for枚举卡上界的数中选了x个1,for枚举不卡上界的数中选了y个1

选出这些数来,从而确定了贡献数对的数量,也确定了下一次卡上界的数的个数,

并算出下一位的n的当前余数,递归到子状态搜索即可

复杂度O(40*256*18*18*18),但跑不满

注意特判k=1(取不出两个数)和m=0(a数组为空)的情况

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=42,M=256,K=19,mod=1e9+7;
ll n,m,dp[N][M][K],c[K][K];
int k,a[N];
ll dfs(int x,int r,int cur){
	if(r>=M)return 0;
	if(x==-1)return r==0;
	if(~dp[x][r][cur])return dp[x][r][cur];
	ll &ans=dp[x][r][cur];ans=0;
	int nb=(x==0?0:(n>>(x-1)&1));
	//printf("x:%d ax:%d\n",x,a[x]);
	if(a[x]==0){
		for(int i=0;i<=k-cur;++i){
			int nr=r-i*(k-i);
			if(nr<0)continue;
			ans=(ans+1ll*c[k-cur][i]*dfs(x-1,(nr<<1)|nb,cur)%mod)%mod;
		}
	}
	else{
		for(int i=0;i<=cur;++i){
			for(int j=0;j<=k-cur;++j){
				int nr=r-(i+j)*(k-(i+j));
				//printf("x:%d r:%d cur:%d i:%d j:%d nr:%d\n",x,r,cur,i,j,nr);
				if(nr<0)continue;
				ans=(ans+1ll*c[cur][i]*c[k-cur][j]%mod*dfs(x-1,(nr<<1)|nb,i)%mod)%mod;
			}
		}
	}
	//printf("x:%d r:%d cur:%d ans:%lld\n",x,r,cur,ans);
	return ans;
}
ll cal(){
	if(k==1)return !n;
	if(!m)return !n;
	memset(dp,-1,sizeof dp);
	int c=0;
	for(ll x=m;x;x>>=1){
		a[c++]=x%2;
	}
	ll rn=0;
	for(int i=c;i<=50;++i){
		if(n>>i&1){
			rn+=1ll<<(i-c);
			if(rn>=M)return 0;
		}
	}
	return dfs(c,(int)rn,k);
}
int main(){
	c[0][0]=1;
	for(int i=1;i<K;++i){
		c[i][0]=c[i][i]=1;
		for(int j=1;j<i;++j){
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
		}
	}
	cin>>n>>m>>k;
	cout<<cal()<<endl;
	return 0;
}

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值