一分钟学会SOS DP,学不会来打我

SOS DP

又叫子集 d p dp dp

有时候会遇到这样的问题,已知数组 a a a,对每一个 m a s k mask mask

f [ m a s k ] = ∑ x & m a s k = x a x f[mask]=\sum\limits_{x\&mask=x}a_x f[mask]=x&mask=xax

你当然可以暴力枚举 m a s k mask mask x x x求,但是这个玩意其实可以 d p dp dp

有点类似 F W T FWT FWT

定义 f [ m a s k ] [ i ] f[mask][i] f[mask][i]表示只有二进制前 i i i位和 m a s k mask mask不同的满足条件的 ∑ a x \sum\limits a_x ax

那么当 m a s k mask mask的第 i i i位为 0 0 0

满足条件的 x x x i i i位也必然为 0 0 0,那么实际上就等价于 f [ m a s k ] [ i − 1 ] f[mask][i-1] f[mask][i1]

因为如果 x x x i i i位和 m a s k mask mask相等,就相当于只有前 i − 1 i-1 i1位不同而已

m a s k mask mask的第 i i i位为 1 1 1

那么满足条件的 x x x i i i位可以为 0 0 0也可以为 1 1 1

如果 x x x i i i位为 1 1 1,那么实际上等价于 f [ m a s k ] [ i − 1 ] f[mask][i-1] f[mask][i1](同上)

如果 x x x i i i位为 0 0 0,我们仍然构造一种方案使得能够类似上面来转移

也就是等价于 f [ m a s k ⊕ 2 i ] [ i − 1 ] f[mask\oplus 2^i][i-1] f[mask2i][i1]

解 释 \color{Red}解释

现在我们要找的 x x x满足 x & m a s k = x x\&mask=x x&mask=x

现在我们暂时不考虑第 i i i位二进制是否满足

那么 f [ m a s k ⊕ 2 i ] [ i − 1 ] f[mask\oplus 2^i][i-1] f[mask2i][i1]中的所有 x x x,不考虑第 i i i位时,仍然满足 x & m a s k = x x\&mask=x x&mask=x

现在考虑第 i i i位,因为 m a s k ⊕ 2 i mask\oplus 2^i mask2i的第 i i i位是 0 0 0

那么 f [ m a s k ⊕ 2 i ] f[mask\oplus 2^i] f[mask2i]中满足条件的 x x x的第 i i i位都是 0 0 0

显然这样的 x x x就是我们所需要的 x x x

所以有转移方程 f [ m a s k ] [ i ] = f [ m a s k ] [ i − 1 ] + f [ m a s k ⊕ 2 i ] [ i − 1 ] f[mask][i]=f[mask][i-1]+f[mask\oplus2^i][i-1] f[mask][i]=f[mask][i1]+f[mask2i][i1]

可以滚动数组优化空间,枚举 i i i,再枚举 m a s k mask mask即可

因为每次从 i , m a s k i,mask i,mask更小的地方转移而来,所以 m a s k mask mask应该倒序枚举保证是上一轮的状态

for(int i = 0; i<(1<<N); ++i)	F[i] = A[i];

for(int i = 0;i < N; ++i)
for (int mask = (1<<N)-1; mask >= 0 ; mask--)
	if (mask & (1 << i))
		F[mask] += F[mask ^ (1 << i)];

但是其实 m a s k mask mask正序枚举也是 可以的…因为 m a s k ⊕ 2 i mask \oplus 2^i mask2i一定是直接继承的上一次的状态,所以不会有影响

for(int mask = 0; mask < (1<<N); ++mask){
	if(mask & (1<<i))	F[mask] += F[mask^(1<<i)];

事实上网上博客基本都是正序来写的…虽然这样没问题,但是不利于理解吧??..


例题

没有链接了,没有评测点

题意

给定 n n n个数,保证 n n n和数字都小于等于 1 0 6 10^6 106

a i & a j = = 0 a_i\&a_j==0 ai&aj==0 ( i , j ) (i,j) (i,j)的数目. i , j i,j i,j无序,贡献算两次

我们把每个数字 x x x的数目叠起来作为 a x a_x ax,然后求

f [ m a s k ] = ∑ x & m a s k = x a x f[mask]=\sum\limits_{x\&mask=x}a_x f[mask]=x&mask=xax就可以了

但是 f [ m a s k ] f[mask] f[mask]表示那些是 m a s k mask mask子集的 a x a_x ax的和

现在我们是求不是子集的和,那么也就是求 m a s k mask mask的补集的 f f f值即可

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1<<20;
int t,n,a[maxn<<1],f[maxn<<1],mx=(1<<20);
int main()
{
	cin >> t;
	while( t-- )
	{
		cin >> n;
		for(int i=1;i<=n;i++)
		{
			int x; scanf("%d",&x);
			a[x]++;
		}
		for(int i=0;i<mx;i++)	f[i] = a[i];
		for(int i=0;i<20;i++)
		{
			for(int j=mx-1;j>=0;j--)
				if( j&(1<<i) )	f[j] += f[j^(1<<i)];
		}
		long long ans = 0;
		for(int i=0;i<=1000000;i++)	ans += 1ll*a[i]*f[(mx-1)^i],a[i]=0;
		cout << ans << endl;
	}
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值