子集相关
枚举子集
https://www.cnblogs.com/CDOI-24374/p/15876755.html
考虑常见枚举子集方式:(\(t\) 不能枚举到 \(0\),否则 \((0-1) \& s\) 会寄。要特判。)
for(int t = s; ; t = (t - 1) & s) {
...
if(t == 0) break;
}
upd:原写法很容易漏掉 0,不能这么写。应当最后当 \(t=0\) 的时候 break。
那么如果是对于 \(n\) 个元素的所有集合求子集呢?时间复杂度是什么?考虑二项式定理:
而可以推出式子,所有集合的子集个数为:
也就是
比较得,\(a=2,b=1\) 的时候 \(3^n = \sum \limits_{i = 0}^n \dbinom{n}{i} 2^i\)。
因此是 \(O(3^n)\) 的。
子集和问题
使用高维前缀和处理子集信息查询问题。
想象高维空间上的几何体得到,令 \(m = n/2\),那么 \(m\) 维空间中,如果做前缀和,那么 \(s_{x_1,...,x_m}\) 就是 \(x_1...x_m\) 的所有子集的权值和。
这个前缀和,除了容斥求解,还可以这样做:对每个维度分别求一次前缀和。
令值域为 \([1,k]\),那么时间复杂度 \(O(m \times k^m)\) 可以预处理所有集合的子集和。其中 \(k^m\) 是高维空间容量。
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
for(int k=1;k<=p;++k)
{
a[i][j][k]+=a[i-1][j][k];
}
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
for(int k=1;k<=p;++k)
{
a[i][j][k]+=a[i][j-1][k];
}
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
for(int k=1;k<=p;++k)
{
a[i][j][k]+=a[i][j][k-1];
}
}
}
对于子集求和问题,\(k=2\),\(0/1\) 分别表示选不选。
for(int i = 0; i <= m; i++) {
for(int j=0;j<(1<<w);++j)//求每个维度的前缀和
{
if(j&(1<<i))s[j]+=s[j^(1<<i)];
}
}
注意这时可以任选每个维度计算前缀和的顺序,因为每一次只会有 \(0 \rightarrow 1\) 这一个过程(无序性有的时候也会用到,比如上次 ABC 的 G)。但是对于一般的前缀和,应当按照拓扑序去计算。也就是说,因为求子集和的过程中,数字大的包含数字小的,所以应该把小的先计算出来。
类似地,求超集和的代码:
f(i, 0, k) {
f(j, 0, n - 1) {
if(!((j >> i) & 1)) {
b[j] ^= b[j ^ (1 << i)];
}
}
}
CF1713F
【题意】
对于大小为 \(n\) 的数组 \(a\),可以以如下方式构造大小为 \((n+1) \times (n+1)\) 的矩阵 \(b\):
- \(\forall 0 \le i \le n, b_{i,0} = 0\)
- \(\forall 1 \le i \le n, b_{0, i} = a_i\)
- \(\forall 1 \le i,j \le n, b_{i,j} = b_{i,j-1} \oplus b_{i-1, j}\)
例如,对于 \(a = [1,2,3]\),\(b\) 为:
\(\mathbf0\) | 1 | 2 | 3 |
---|---|---|---|
\(\mathbf0\) | 1 | 3 | 0 |
\(\mathbf0\) | 1 | 2 | 2 |
\(\mathbf0\) | 1 | 3 | 1 |
给定 \(c\) 数组满足 \(c_i = b_{i, n}, 1 \le i \le n\)。求出 \(a\)。
【分析】
首先,对于给定的 \(c\),一定有唯一一个 \(a\)。
然后我们考虑 \(a_i\) 对 \(c_i\) 的贡献。
我们标记 \(a_1 = 1\),以计算它的贡献。可以得到这样的一个矩阵:(一个数的值等于上面和左边的异或)
\(\mathbf0\) | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|
\(\mathbf0\) | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
\(\mathbf0\) | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
\(\mathbf0\) | 1 | 1 | 0 | 0 | 1 | 1 | 0 |
\(\mathbf0\) | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
\(\mathbf0\) | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
\(\mathbf0\) | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
\(\mathbf0\) | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
这叫谢尔宾斯基三角形,是一个著名的分形。但是我们不管他,我们发现这不是组合数模 \(2\) 吗?递推式证明确实是这样。对于 \(i \ge 1\),就是把整个形状向右移动得来。
考虑 \(a_i\) 对于 \(b_{j, n}\) 的贡献。分析行列号得到 \(\dbinom{n+j-i}{j}\)。这里 \(i,j\) 已经从 \(0\) 标号。考虑对下标做变换,令 \(i \rightarrow n - i\)。那么得到 \(\dbinom{i+j}{j}\)。
我们由卢卡斯定理得到,如果 \(\dbinom{n}{m} \bmod 2 =1\),那么对于 \(n,m\) 拆位后的每一位,都有 \(\dbinom{n_i}{m_i} \% 2 = 1\)。也就是 \(\dbinom{0}{1} \neq \dbinom{n_i}{m_i}\),也就是 \(n\) 在高维空间内是 \(m\) 的超集(超集是子集的逆运算)。
这个结论在 [CTSC2017] 吉夫特一题中出现,是卢卡斯定理的经典应用。
由 \((i+j) \& j = j\),我们可以推出 \(i \& j = 0\)。这是由于进位:
如果像这样出现了两个 \(1\),那么 \((i+j)\) 这位数是 \(0\),而 \(j\) 这位数是 \(1\),不可以。因此只能是 \(i,j\) 不能同时出现 \(1\)。
这又是一个经典结论。
考虑 \(a\) 知道的情况下,能不能求 \(c\)。考虑 \(a_i \rightarrow c_j\),由于限定了 \(i = j = 1\) 不成立,我们可以转化为 \(i_k \le (\sim j_k)\)。也就是一个子集和的形式。求出 \(d_i = \sum \limits_{j \subseteq i} a_j\),那么 \(c_i\) 就等于 \(d_{\sim i}\)。
那倒着做能不能行呢?我们考虑子集反演式子,它长这样:
证明:考虑容斥原理。对于集合 \(Q \subseteq U\),计算它的贡献次数。若 \(Q \not \subseteq S\),那么显然计算了 \(0\) 次。否则,令 \(|S| - |T| = k\),这里的 \(|S|\) 指 \(S\) 中 \(1\) 的个数。那么一共有 \(\dbinom{k}{i}\) 个集合 \(Q\) 满足 \(|S| - |Q| = i\),总贡献为 \(\sum \limits_{i = 0}^k (-1)^i \dbinom{k}{i} = (-1 + 1)^k = 0^k\),因为利用容斥原理的式子中考虑 \(0^0=1,0^i=0(i\neq 0)\),因此当且仅当 \(k = 0\) 的时候计算了一次。
其实基本所有容斥原理的式子都是和 \((-1+1)^k\) 有关的,要掌握。
对于异或,没有了 \(-1\) 的符号限制,因此子集异或和的逆是子集异或和,超集异或和的逆是超集异或和。因此这个容斥式子可以转化为 FWT 板子。
有了子集反演的式子,正常来说是可以,但是这里的 \(d_i\) 没给全啊!\(c\) 数组按道理应该有 \(2^{\lceil\log n\rceil}\) 的长度,但是这里只给了 \(n\) 的长度,无法还原。例如 \(n = 10\),我们只知道 \(d_{6,...,15}\),不知道前五个数,失败了。
我们应该换一个思路。依然考虑容斥。既然要求 \(i=1\) 的地方 \(j\) 都不能为 \(0\),那么我们考虑应用超集的工具。考虑钦定一个包含 \(k\) 个位置的集合 \(P\),满足 \(P\) 中每位都有 \(i = 1\) 并且必填 \(j=1\),其他位随便,那么这个集合内的元素异或和也就是 \(P\) 的超集和。考虑一个和 \(i\) 有 \(x\) 个位置都为 \(1\) 的 \(t\) 有几个超集包含了。考虑这 \(x\) 个位置在 \(P\) 里出现任意子集,其他位置在 \(P\) 里一律不能出现(因为这些位置都填的 \(0\))。也就是贡献为 \(\sum \limits_{i = 0}^x \dbinom{x}{i} (-1)^x\)。显然只有 \(x=0\) 的时候存在一次贡献。那么对于符合条件的 \(a_j\),会对 \(b_i\) 产生一次贡献。写成式子就是:
也就是说,对 \(a\) 进行一次超集和变成 \(c\),对 \(c\) 进行一次子集和变成 \(b\)。逆着做,先子集和再超集和即可。
另一种理解方法:(tyy 的神思路)我们先把 \(a\) 贡献到对角线上,再由对角线贡献到 \(b\)。为了方便,把 \(a\) 和对角线从右往左重编号为 $0,1,\cdots,n-1$0,那么 \(a\) 的第 \(i\) 个位置会对对角线上的第 \(j\) 个位置贡献 \(\binom{i}{j}\) 次。同理可得由对角线推 \(b\) 只需要对对角线求子集和。现在知道 \(b\),那对 \(b\) 做一次子集和的逆再做一次超集和的逆就得到了 。由于是异或,子集和的逆即为子集和,超集和的逆即为超集和。这样我们就以 \(O(n\log n)\) 的复杂度解决了本题。
【代码实现】
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int b[2000010];
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//time_t start = clock();
//think twice,code once.
//think once,debug forever.
int n; cin >> n;
int k = log2(n) + 1;
f(i, 0, n - 1) cin >> b[i];
f(i, 0, k) {
for(int j = n - 1; j >= 0; j --) {
if((j >> i) & 1) {
b[j] ^= b[j ^ (1 << i)];
}
}
}
f(i, 0, k) {
f(j, 0, n - 1) {
if(!((j >> i) & 1)) {
b[j] ^= b[j ^ (1 << i)];
}
}
}
for(int i = n - 1; i >= 0; i --) cout << b[i] << " ";
//time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
吉夫特
这题做法是 \(n 2^{\log n}\)(每个元素可以相同)/ \(3^n\) 的。但是我们注意到查询和贡献的时间不对等,考虑根号平衡。
原来的式子是 \(dp_i = \sum \limits_{j \subseteq i} dp_j\),但是我们可以将 \(i\) 拆成 \(i % 2^9\) 和 \(i / 2^9\) 两个数,然后前一半算完 dp 转移超集和;后一半从子集转移来,这样时间变成了 \(O(n 2^{n/2})\)(可相同)或者 \(O(6^{n/2})\)(不相同)的。
超集和这么写:
for(int t = s; t <= (limit); t = (t + 1) | s)
#include<bits/stdc++.h>
using namespace std;
#define int long long
//use ll instead of int.
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
const int inf = 1e9;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; }
reverse(s.begin(), s.end()); cerr << s << endl;
return;
}
template <typename TYP> void cmax(TYP &x, TYP y) {if(x < y) x = y;}
template <typename TYP> void cmin(TYP &x, TYP y) {if(x > y) x = y;}
//调不出来给我对拍!
//use std::array.
int a[233344];
int dp[2050][2050];
int f[2050][2050]; const int mod = 1e9 + 7;
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//freopen();
//freopen();
//time_t start = clock();
//think twice,code once.
//think once,debug forever.
int n; cin >> n;
f(i, 1, n) {
cin >>a[i];
}
reverse(a + 1, a + n + 1); int ans = mod - n;
for(int i = 1; i <= n; i ++) {
int ii = a[i] / (1 << 9), ij = a[i] % (1 << 9);// cerr << ii << " " << ij << endl;
dp[ii][ij] = 1;
for(int j = ij; ; j = (j - 1) & ij) {dp[ii][ij] += f[ii][j]; if(dp[ii][ij] >= mod) dp[ii][ij] -= mod; if(j == 0) break;}
for(int j = ii; j <= (1 << 9); j = (j + 1) | ii) {f[j][ij] += dp[ii][ij]; if(f[j][ij] >= mod) f[j][ij] -= mod;}
ans += dp[ii][ij]; if(ans >= mod) ans -= mod;
}
cout << ans << endl;
//time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
/*
2023/x/xx
start thinking at h:mm
start coding at h:mm
finish debugging at h:mm
*/