高维前缀和(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\) 所有数因数/倍数的某些信息,例如,对每个数求出出现在给定集合中因数的个数
把每个数质因数分解,\(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]];
应用
容斥的思想,与为 \(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;
}