SOSDP
\(\text{SOSDP: Sum over Subsets DP}\)
介绍
例子:给定一个序列长度为 \(2^n\) 的序列 \(a\),求对于 \(i = 0,1,\cdots,2^n-1\) 求 \(A_i\),其中 \(A_i = \sum\limits_{j\&i=j}a_j\),即 \(i\) 的子集和。
设 \(f_{i,s}\) 表示 \(s\) 的前 \(i+1\) 个低位子集的和,有:
初始化 \(f_{0, i} = a_i + a_{i\oplus 1}\)。时间复杂度 \(O(n2^n)\),空间复杂度 \(O(n2^n)\)
rep(i, 1, 1 << n) f[0][i] = a[i] + a[i ^ 1];
rep(i, 1, n - 1) rep(j, 0, (1 << n) - 1)
{
f[i][j] = f[i - 1][j];
if((j >> i) & 1) f[i][j] += f[i - 1][j ^ (1 << i)];
}
\(f_i\) 的转移只和 \(f_{i-1}\) 有关,考虑优化掉这一位。
当 \((s>>i)\&1=1\) 时,\(s \oplus 2^i < s\),可以将 \(s\) 这一维倒序枚举:
rep(i, 0, 1 << n) f[i] = a[i];
rep(i, 1, n) per(j, 0, (1 << n) - 1) if((j >> i) & 1) f[j] += f[j ^ (1 << i)];
再仔细想一下,在第 \(i\) 轮时,\(f_j\) 都是由 \(f_{j\oplus 2^i}\) 转移来的,更新为第 \(i\) 层的 \(f_j\) 的 \(j\) 的第 \(i\) 位都是 \(1\),第 \(i-1\) 层的 \(f_{j\oplus 2^i}\) 的 \(j\oplus 2^i\) 的第 \(i\) 为都是 \(0\)。所以正序枚举 \(j\) 也是可以的。
rep(i, 0, 1 << n) f[i] = a[i];
rep(i, 1, n) rep(j, 0, (1 << n) - 1) if((j >> i) & 1) f[j] += f[j ^ (1 << i)];
时间复杂度 \(O(n2^n)\),空间复杂度 \(O(2^n)\)
以上是求 \(i\) 的子集的和,要是求超集,可以把二进制的 \(0\) 看做 \(1\),\(1\) 看做 \(0\) 来做一次 \(DP\):
rep(i, 0, 1 << n) f[i] = a[i];
rep(i, 1, n) rep(j, 0, (1 << n) - 1) if(!((j >> i) & 1)) f[j] += f[j ^ (1 << i)];
\(\text{SOSDP}\) 与高维前缀和的关系:
给定 \(n\) 维序列 \(a\),求所有 \(S\),其中
若每一维的大小为 \(2\),则可以状态压缩,然后用 \(\text{SOSDP}\) 来做。
高维后缀和就是求超集。
例题
1. ARC100C Or Plus Max
设 \(f_s\) 为满足 \(i | j \le s,i\not=j\) 最大值 \(a_i\) 和次大值 \(a_j\),然后 \(f\) 做一个前缀 \(\max\) 就为答案。
bool _st;
int n, a[1 << 18];
struct T {
int mx, mxs;
void operator += (const T& rhs) {
cmax(mxs, rhs.mxs);
if(mx <= rhs.mx) {
cmax(mxs, mx);
mx = rhs.mx;
} else if(mxs < rhs.mx)
mxs = rhs.mx;
}
} f[1 << 18];
bool _ed;
int main() { FIN ::std::cerr << (&_ed - &_st) / 1024.0 / 1024 << ::std::endl;
qr(n);
int sn = (1 << n) - 1;
rep(i, 0, sn) qr(a[i]);
rep(i, 0, sn) f[i] = {a[i], 0};
Rep(i, 0, n) rep(j, 0, sn) if((j >> i) & 1) f[j] += f[j ^ (1 << i)];
int ans = 0;
rep(i, 1, sn) cmax(ans, f[i].mx + f[i].mxs), qw(ans), pn;
return 0;
}
2. CF165E Compatible Numbers
\(x\&y=0 \Leftrightarrow y \subseteq \sim x\) 即 \(y | \sim x = ~x\)
则将所有 \(a_i\) 做一遍高维前缀和,查询的时候查 \(\sim a_i\)
bool _st;
int n, f[1 << 22], a[1 << 22];
bool _ed;
int main() { FIN ::std::cerr << (&_ed - &_st) / 1024.0 / 1024 << ::std::endl;
qr(n);
memset(f, 0xFF, sizeof(f));
rep(i, 1, n) qr(a[i]), f[a[i]] = a[i];
int A = (1 << 22) - 1;
rep(i, 0, 22) rep(j, 0, A) if(((j >> i) & 1) && f[j ^ (1 << i)] != -1) f[j] = f[j ^ (1 << i)];
rep(i, 1, n) qw(f[A & ~a[i]]), ps;
return 0;
}
3. The 2024 ICPC Asia Hangzhou Regional Contest J. Japanese Bands
设 \(A_i = \{a_i, b_i\}\)
设 \(S\) 和 \(T\) 分别为角色卡和音乐卡上出现的数字的集合,状态压缩。
\(S,T\) 满足:
- \(\forall i, A_i \cap S \not= \varnothing\)
- \(\forall i, A_i \cap T \not= \varnothing\)
- \(\bigcup\limits_iA_i \subseteq S\cup T\)
用隔板法可以求出答案为:
对于限制 \(1\)、\(2\):
\(A_i \cap S=\varnothing \Leftrightarrow A_i\cap\overline{S}=A_i\)
可以对所有的 \(A\) 做一个高维前缀和,然后看 \(\overline S\) 是否合法。具体来说是设 \(f_i\) 为集合 \(i\) 是否可以由 \(A\) 表示出,用 \(\text{SOSDP}\) 可以在 \(O(m2^m)\) 求出,若 \(S\) 满足限制 \(1\),则 \(f_{\overline S} = 0\)
对于限制 \(3\):
设 \(B = \bigcup\limits_iA_i\),有 \(B\subseteq S\cup T \Leftrightarrow B-B\cap S \subseteq T\)
可以对所有的 \(A\) 做个高维后缀和,然后查找 \(B-B\cup S\) 对应的值。具体来说是设 \(g_i\) 为 \(A\) 可表示出的集合的贡献和,且这样的集合的子集有 \(i\),用 \(\text{SOSDP}\) 可以在 \(O(m2^m)\) 求出。
枚举合法的 \(S\),然后查询 \(g_{B-B\cap S}\) 的贡献。
每次预处理组合数,总时间复杂度 \(O(m2^m)\)
bool _st;
#define popc __builtin_popcount
ll inv[21];
ll C(int x, int y) {
if(x < 0 || y < 0 || x < y) return 0;
ll res = 1;
rep(i, x - y + 1, x) res = res * i % M * inv[x - i + 1] % M;
return res;
}
ll n1, n2, n, m, T[1 << 20], Cn1[21], Cn2[21];
int s[405], a[405], b[405], S[1 << 20];
void solve() {
qr(n1), qr(n2), qr(m), qr(n);
int B = 0;
rep(i, 1, n)
{
qr(a[i]), qr(b[i]);
s[i] = (1 << (a[i] - 1)) | (1 << (b[i] - 1));
B |= s[i];
}
rep(i, 0, m)
Cn1[i] = C(n1 - 1, i - 1),
Cn2[i] = C(n2 - 1, i - 1);
rep(i, 1, n) S[s[i]] = 1;
Rep(i, 0, m)
Rep(j, 0, 1 << m)
if((j >> i) & 1)
S[j] |= S[j ^ (1 << i)];
int A = (1 << m) - 1;
Rep(i, 0, 1 << m)
if(!S[i])
T[A ^ i] = Cn2[popc(A ^ i)];
Rep(i, 0, m)
Rep(j, 0, 1 << m)
if(!((j >> i) & 1))
ADD(T[j], T[j ^ (1 << i)]);
ll ans = 0;
Rep(i, 0, 1 << m)
if(!S[i])
ADD(ans, Cn1[popc(A ^ i)] * T[B ^ (B & (A ^ i))] % M);
qw(ans), pn;
Rep(i, 0, 1 << m) S[i] = T[i] = 0;
}
bool _ed;
int main() { FIN ::std::cerr << (&_ed - &_st) / 1024.0 / 1024 << ::std::endl;
rep(i, 0, 20) inv[i] = cksm<M>(i, M - 2);
int tcs;
qr(tcs);
while(tcs--) solve();
return 0;
}

浙公网安备 33010602011771号