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_{i,s} = \begin{cases} f_{i-1,s}, & (s>>i)\&1=0\\ f_{i-1,s} + f_{i-1, s \oplus 2^i}, & (s>>i)\&1=1 \end{cases} \]

初始化 \(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\),其中

\[S_{i_1,i_2,\cdots,i_n} = \sum\limits_{j_1\le i_1,j_2\le i_2,\cdots,j_n\le i_n}a_{j_1,j_2,\cdots,j_n} \]

若每一维的大小为 \(2\),则可以状态压缩,然后用 \(\text{SOSDP}\) 来做。

高维后缀和就是求超集。

例题

1. ARC100C Or Plus Max

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

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

洛谷题面 P14196

\(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\)

用隔板法可以求出答案为:

\[\begin{aligned} \sum\limits_S\sum\limits_T\binom{n_1-1}{ |S| -1}\binom{n_2-1}{ |T| -1}\\ =\sum\limits_S\binom{n_1-1}{ |S| -1}\sum\limits_T\binom{n_2-1}{ |T| -1} \end{aligned} \]

对于限制 \(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;
}
posted @ 2025-10-20 15:12  kuailedetongnian  阅读(10)  评论(0)    收藏  举报