高维前缀和(SOS DP)

高维前缀和(SOS DP)

通常求二维前缀和,用容斥来求

但其实,完全可以先做一遍行的前缀和,再做一遍列的前缀和

拓展到 \(k\) 维也是如此,可以在 \(O(nk)\) 的复杂度求前缀和

但怎么和 DP 扯上关系?

可以把第 \(i\) 维当作阶段,每一维的具体信息是状态

先枚举阶段,表示当前固定其它维,只统计这一维的贡献,再枚举状态,根据是求前缀和还是后缀和转移

子集求和

它的思想可以用来解决子集求和类问题

如果想对所有 \(i\in[0,2^n)\),求 \(f_i=\sum_{j\subseteq i}a_j\)

一维一维的处理,枚举第 \(j\) 位,如果第 \(j\) 位是 \(1\),就加上第 \(j\) 位不是 \(1\)\(f\)

其实 \(f\) 滚动掉了阶段这一维,原始的 DP 应是 \(dp(i,s)\) 为处理了状态为 \(s\),二进制前 \(i\) 位的答案,第 \(i\) 位若为 \(1\),可以在继承 \(dp(i-1,s)\) 的基础上选择加上第 \(i\) 位为 \(0\) 的子集

超集同理,如果第 \(j\) 位是 \(0\),就加上第 \(j\) 位是 \(1\)\(f\)

这里是超集的代码:

for(ll j = 0; j < 20; ++j) // 高维前缀和,超集 
	for(ll i = mx; i >= 0; --i) // f[i] 为包含 i的和(有多少个数 & i = i) 
		if(!((i >> j) & 1))	f[i] += f[i ^ (1ll << j)]; 
// 倒序枚举:DP数组其实压了一维,用到了比 i大的信息 

\(\min,\max\) 同样可做

子集或超集都暗含着偏序关系,求前缀和也如此,每一维都要比当前的小,才可被加入当前的前缀和

Dirichlet 前缀和

可以在 \(O(n\log\log n)\) 的复杂度内求出 \(1\sim n\) 所有数因数/倍数的某些信息,例如,对每个数求出出现在给定集合中因数的个数

P5495 Dirichlet 前缀和

把每个数质因数分解,\(x=\sum_{i=1}^k p_i^{a_i},y=\sum_{i=1}^k p_i^{b_i}\)

\(x\)\(y\) 产生贡献,当且仅当 \(\forall i\in[1,k],a_i\le b_i\)

本质就是高维前缀和,先枚举质数,再枚举具体是哪个数,注意从小到大枚举

for(ui j = 1; j <= cnt; ++j)
    for(ui i = 1; i * prime[j] <= n; ++i)   a[i * prime[j]] += a[i];

求倍数的:

for(ll j = 1; j <= cnt; ++j)
    for(ll i = V / prime[j]; i > 0; --i)    num[i] += num[i * prime[j]];

应用

CF449D Jzzhu and Numbers

容斥的思想,与为 \(0\) 不好做,用全集 \(-\) 与不为 \(0\) 的情况,因为不为 \(0\) 说明指定的几位必须为 \(1\),好处理

枚举指定哪几位为 \(1\),如果指定的位数为奇数,容斥系数为 \(-1\),否则为 \(1\)

然后就是求超集的高维前缀和,\(f_i=\sum_{j}[i\subseteq a_j]\)

int main()
{
	n = read(), pw[0] = 1;
	for(ll i = 1; i <= n; ++i)	a[i] = read(), ++f[a[i]];
	for(ll i = 1; i <= n; ++i)	pw[i] = add(pw[i - 1], pw[i - 1]);
	for(ll j = 0; j < 20; ++j) // 高维前缀和,超集 
		for(ll i = mx; i >= 0; --i) // f[i] 为包含 i的和(有多少个数 & i = i) 
			if(!((i >> j) & 1))	f[i] += f[i ^ (1ll << j)]; // 倒序枚举:DP数组其实压了一维,用到了比 i大的信息 
	for(ll i = 1; i <= mx; ++i)
		if(popcnt(i) & 1)	ans = add(ans, add(pw[f[i]], mod - 1));
		else	ans = add(ans, add(mod - pw[f[i]], 1));
	printf("%lld", add(add(pw[n], mod - 1), mod - ans)); // 容斥,总方案数-&至少有一位+&至少有两位…… 
	return 0;
}

CF1614D2 Divan and Kostomuksha (hard version)

用类似后缀和的方法求出每个数出现在 \(a\) 序列中倍数的数量,记作 \(num_i\)

考虑 DP,由于 \(\gcd\) 变化时,后一个一定时前一个的因数

因此如果知道上一个 \(\gcd\ i\) 以及下一个 \(\gcd\ j\),可以用 \(num\) 求出能摆的数有 \(num_j-num_i\) 个,因为是 \(i\) 倍数的一定也是 \(j\) 的倍数,是 \(i\) 倍数的在 \(i\) 处已经被用了,而且 \(i>j\),在 \(i\) 处用更优

于是设 \(f_i\) 表示末尾 \(\gcd\)\(i\) 倍数时的最大贡献,枚举 \(i\) 的倍数 \(j\) 转移,\(f_i=\max\{f_j+i\times(num_i-num_j)\}\)

初始时 \(f_i=num_i\times i\),转移从大到小

这样只能通过 Easy version

发现如果 \(j=p_1\times p_2\times\dots\times p_k\times i\)\(p_1,p_2,\dots p_k\) 为质数,则 \(j\)\(i\) 的贡献被重复枚举

如果一次只多一个质数,例如 \(f_j\) 先贡献给 \(f_{p_2\times\dots\times p_k\times i}\),再贡献给 \(f_{p_3\times\dots\times p_k\times i}\),这样转移去除了很多重复工作,且不会漏掉

于是每次枚举质数 \(p_j\)\(f_i\)\(f_{i\times p_j}\) 转移,复杂度同埃拉托色尼筛法,为 \(O(n\log\log n)\)

int main()
{
    read(n);
    for(ll i = 1; i <= n; ++i)  read(a[i]), ++num[a[i]];
    for(ll i = 2; i <= V; ++i)
    {
        if(!st[i])  prime[++cnt] = i;
        for(ll j = 1; j <= cnt && i * prime[j] <= V; ++j)
        {
            st[i * prime[j]] = 1;
            if(i % prime[j] == 0)   break;
        }
    }
    for(ll j = 1; j <= cnt; ++j)
        for(ll i = V / prime[j]; i > 0; --i)    num[i] += num[i * prime[j]];
    for(ll i = 1; i <= V; ++i)  f[i] = i * num[i];
    for(ll i = V; i > 0; --i)
    {
        for(ll j = 1; j <= cnt && i * prime[j] <= V; ++j)
            f[i] = max(f[i], f[i * prime[j]] + i * (num[i] - num[i * prime[j]]));
        if(num[i] == n) ans = max(ans, f[i]);
    }
    cout << ans;
    return 0;
}
posted @ 2024-02-14 22:24  KellyWLJ  阅读(120)  评论(0编辑  收藏  举报