NOIP 2021 数列(dp+二进制相关)(读者 rp +++++++)

NOIP 2021 数列 (dp+二进制相关)(读者 rp +++++++)

先剧透下,这道题复杂度 O ( n 4 m ) \Omicron(n^4m) O(n4m)

快来看看吧

题意

给定 n , m , k n,m,k n,m,k 和一个长度为 m + 1 ( 0 ∼ m ) m+1(0 \sim m) m+1(0m) 的正整数数组 v v v

对于一个长度为 n ( 1 ∼ n ) n(1 \sim n) n(1n) 的序列 a a a 0 ≤ a i ≤ m 0 \le a_i \le m 0aim),权值为 ∏ i = 1 n v a i \prod_{i=1}^{n} v_{a_i} i=1nvai

当序列 a a a 满足 p o p c o u n t ( ∑ i = 1 n 2 a i ) ≤ k popcount(\sum_{i=1}^{n} 2^{a_i}) \le k popcount(i=1n2ai)k1 a a a 为一个合法序列。

求所有合法序列的权值和 % 998244353 \%998244353 %998244353

解法

很有意思的一道 dp

先钦定 a a a 为升序,这样好操作一点,最后再乘上方案数就好。

f i , j , k , l f_{i,j,k,l} fi,j,k,l 表示选到 a i a_i ai a i a_i ai 已经选完),当前最高位为 j j j (即最大选了 j − 1 j-1 j1 ),选了 k k k 个,状态中有 l l l 1 1 1

初始化 f 0 , 0 , 0 , 0 = 1 f_{0,0,0,0}=1 f0,0,0,0=1

首先枚举 i , j , k , l i,j,k,l i,j,k,l ,然后再枚举一维 p ( 0 ∼ n − i ) p(0 \sim n-i) p(0ni) ,意为往后面再放 p p p j j j

那么 i + 1 ∼ i + p i+1 \sim i+p i+1i+p 就全部放好了;最高位也由 j j j 变成了 j + 1 j+1 j+1 (选得最大的数由 j − 1 j-1 j1 变成了 j j j );由于上一位选了 k k k 个,加上当前选的 p p p 个,那进位后就变成了 ⌊ k + p 2 ⌋ \lfloor \frac{k+p}{2} \rfloor 2k+p ;同时如果有余数,状态中为 1 1 1 的就 + 1 +1 +1 ,所以为 l + ( k + p ) % 2 l+(k+p)\%2 l+(k+p)%2

而因为选了 p p p j j j ,所以贡献也要加上 p p p j j j 的,所以在输入时预处理,把 v v v 数组改成二维, v i , j v_{i,j} vi,j 表示 i i i 选了 j j j 个的贡献。

那么状态转移方程即为

f[i+p][j+1][(k+p)>>1][l+(k+p)%2]+=f[i][j][k][l]*v[j][p];

不对不对,还要乘上方案数呢?

上面卡掉,重新来:

f[i+p][j+1][(k+p)>>1][l+(k+p)%2]+=f[i][j][k][l]*v[j][p]*c[i+p][p];

c c c 就是组合数, c i , j c_{i,j} ci,j 表示 C i j C_i^j Cij ,可以提前 O ( n 2 ) \Omicron(n^2) O(n2) 预处理(应该都会,不再赘述,不会的看代码)

那么最后的答案为 ∑ i = 0 n ∑ j = 0 , p o p c o u n t ( i ) + j ≤ K K f n , m + 1 , i , j \sum_{i=0}^{n} \sum_{j=0,popcount(i)+j \le K}^{K} f_{n,m+1,i,j} i=0nj=0,popcount(i)+jKKfn,m+1,i,j

代码

#include<cstdio>
#define ll long long int 
using namespace std;
const ll Md=998244353;
const int N=30,M=100;
ll c[N+10][N+10],v[M+10][N+10],f[N+10][M+10][N+10][N+10];
int cnt(int x)
{
	int s=0;
	while(x)
	{
		s+=x%2;
		x>>=1;
	}
	return s;
}
int main()
{
	int n,m,K;
	scanf("%d%d%d",&n,&m,&K);
	for(int i=0;i<=n;i++)
	{
		c[i][0]=1;
		for(int j=1;j<=i;j++)
		{
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%Md;
		}
	}
	for(int i=0;i<=m;i++)
	{
		v[i][0]=1;
		scanf("%lld",&v[i][1]);
		for(int j=2;j<=n;j++)
		{
			v[i][j]=(v[i][j-1]*v[i][1])%Md;
		}
	}
	f[0][0][0][0]=1;
	for(int i=0;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			for(int k=0;k<=(i>>1);k++)
			{
				for(int l=0;l<=K;l++)
				{
					for(int p=0;p<=n-i;p++)
					{
						f[i+p][j+1][(k+p)>>1][l+(k+p)%2]=(f[i+p][j+1][(k+p)>>1][l+(k+p)%2]+((f[i][j][k][l]*v[j][p])%Md*c[i+p][p])%Md)%Md;
					}
				}
			}
		}
	}
	ll ans=0;
	for(int i=0;i<=n;i++)
	{
		for(int j=0;j<=K;j++)
		{
			if((cnt(i)+j)<=K)
			{
				ans=(ans+f[n][m+1][i][j])%Md;
			}
		}
	}
	printf("%lld",ans);
	return 0;
}

后记

终于打完了,推状态转移方程那部分自认为还是打的蛮详细蛮用心的

大家也要用心看吖

不要忘记一键三连哦

谢谢🍟(用意念请大家吃薯条)


  1. p o p c o u n t ( i ) popcount(i) popcount(i) 表示 i i i 在二进制下 1 1 1 的个数 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值