子集和DP

这个东西好玄学……

巧了Catherine就喜欢玄学!

总结的话,等我再刷两个T再upd。可能以后再upd吧……

void fwt_or(int f[], int opt)
{
    opt = (opt + mod) % mod;
    for(int l=2,k=1; l<=len; l<<=1,k<<=1)
        for(int i=0; i<len; i+=l)
            for(int j=0; j<k; j++)
                f[i+j+k] = (f[i+j+k]+1ll*opt*f[i+j]%mod)%mod;
}

这东西我不会,只是想装X。

B. 放进去

对每个物品分别考虑(假设只有一行),选择一个集合,贡献就是其中对应最小的a,用二进制数表示1表示选了0表示没选,最后的答案就是f[s]+sumb[s]。对每个集合求sumb的话按lowbit拆开算就好了,主要是这个f比较复杂。

预处理:把a降序排列,如果当前的前缀 s 包含 i ,f[s] += a[i],对不包含 i 的上一个前缀减掉a[i]。这样的话如果从后往前(按操作顺序,其实就是每次跳回上一个子集)求和,就相当于先假设最小值是全局最小a[min],回到上一个子集加上a[cmin]-a[min](cmin表示次小值),就是推翻原来的假设再新建一个。

把操作顺序看成时间观察这些状态,时间从后往前就是最小值从小到大依次缺失。

每个f[s]最终的结果就是s的超集对应的f全部求和。

  • 为什么是超集求和?

如果某一列没有选,它一定不会成为最小值,但是把它选上它也不一定是最小值,只有当它以及比它a更小的位置全都为0的,不选这一列才会对答案造成影响(使这一行的最小值变大)。

要找到答案变成了什么,就是找到最小值缺失的程度(从谁以下更小的数都不被包含在集合),把当前集合和预处理得到的这些状态进行“匹配”,如果当前集合是预处理出的某些状态的子集,设这些状态中时间最早的是 x ,(按照时间排列预处理得到的状态形成一个序列) x 的后缀和就是不断推翻对最小值的假设知道第 x 大的数(降序排列的第x个)作为最小值对当前状态成立。

由于物品很多,每组物品排序的结果有很多种,我们选择把所有的超集拿出来求和,没有预处理过的状态 f 值为0多加没有影响,分开考虑 f 不为0的所有状态恰好就是上文从 x 开始累记的“后缀和”,因为对于每个物品都是求和运算,可以把状态合并。

  • 怎么快速得到所有超集的和?

所有的状态都被压成了 m 位的二进制数,设第一层循环到了 i ,这一轮结束后的效果是每个数都变成了只考虑最后 i 位的超集和,每一个第 i 位是0的状态都累加上的末 i-1 位的答案。

把每个状态单独拿出来就是:

可以转移到 00000 的有 00001,00010,00100,01000……

转移到 00001 的有 00011,00101,01001,10001……

转移到 00010 的有 00011,00110,01010,10010……

转移到 00011 的有 00111,01011,10011……

转移到 00110 的有 00111,01110,10110……

规律比较明显,就是每次尝试往每一位加1,本来就是1的跳过。

如果按照循环的顺序考虑的话:

省略前面的 f ,第一轮 0000 += 0001,0010 += 0011,0100 += 0101,0110 += 0111……

第二轮 0000 += 0010 (0010 = 0010 + 0011),0001 += 0011,0100 += 0110(0110 = 0110 + 0111)……

这个形式就像统计后缀和,每次只加它的下一项,和前缀和的统计方式同理,拿前缀和来举例子,对于高维前缀和,计算方式如下:

code
 for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        for(int p=1;p<=k;p++)
        	a[i][j][p]+=a[i-1][j][p];
for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        for(int p=1;p<=k;p++)
        	a[i][j][p]+=a[i][j-1][p];
for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        for(int p=1;p<=k;p++)
        	a[i][j][p]+=a[i][j][p-1];

高维前缀和开了很多数组,把每一维分开计算;回到这个题,二进制数的每一位就像一个[],m 位二进制数看成求 m 维后缀和,对每一维(二进制数的每一位)考虑都有下一项,每一位的下一项就是在这一位上加1,如果这一位本来就是1那么它不存在下一项它的当前维后缀和就是自己跳过就好了。把自己跳过的具体实现就是用两层循环把自己这一位岔开。

于是问题解决,代码来自Chen_jr大佬%%%%%%%%%%%。

code
 #include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 34000000;

int n, m, a[30], lg[maxn], p[30], b[30];
ll sb[maxn], f[maxn];

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

bool cmp(int x, int y) {return a[x] < a[y];}

int main()
{
    freopen("putin.in", "r", stdin);
    freopen("putin.out", "w", stdout);
    
    n = read(); m = read();
	for(int i=1; i<=m; i++) p[i] = i - 1;
	for(int i=1; i<=n; i++)
	{
		for(int j=0; j<m; j++) a[j] = read();
		sort(p+1, p+1+m, cmp);
		for(int s=0,j=m; j>0; j--)
		{
			f[s] -= a[p[j]]; s |= (1<<p[j]); f[s] += a[p[j]];
		}
	}
	for(int i=0; i<m; i++)
	{
		for(int j=0; j<(1<<m); j+=(1<<i+1))
		{
			for(int k=0; k<(1<<i); k++)
			{
				f[j+k] += f[j+k+(1<<i)];
			}
		}
	}
	for(int i=1; i<=m; i++) b[i] = read();
	for(int i=2; i<(1<<m); i+=i) lg[i] = lg[i>>1] + 1;
	for(int i=1; i<(1<<m); i++) sb[i] = sb[i^(i&-i)] + b[lg[i&-i]+1];
	ll ans = 4e18;
	for(int s=1; s<(1<<m); s++) ans = min(ans, sb[s]+f[s]);
	printf("%lld\n", ans);

    return 0;
}

 

posted @ 2022-11-14 17:42  Catherine_leah  阅读(67)  评论(2编辑  收藏  举报
/* */