子集相关

枚举子集

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+b)^n = \sum \limits_{i = 0}^ n \dbinom{n}{i} a^i b^{n - i} \]

而可以推出式子,所有集合的子集个数为:

\[\sum \limits_{T \subseteq S} 2^{|T|} \]

也就是

\[\sum \limits_{i = 0}^n \dbinom{n}{i} 2^i \]

比较得,\(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\)。这是由于进位:

\[\begin{array} \right 001011\\ +101100\\ ———— \\ 110111 \end{array} \]

如果像这样出现了两个 \(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}\)

那倒着做能不能行呢?我们考虑子集反演式子,它长这样:

\[f(S) = \sum \limits_{T \subseteq S} g(T) \Rightarrow g(S) = \sum \limits_{T \subseteq S} (-1)^{|S| - |T|} f(T) \]

证明:考虑容斥原理。对于集合 \(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\) 产生一次贡献。写成式子就是:

\[b_i = \sum \limits_{j \subseteq i} \sum \limits_{k \supseteq j} a_k \]

也就是说,对 \(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
*/
posted @ 2022-12-18 11:39  OIer某罗  阅读(26)  评论(0编辑  收藏  举报