loading...qwq

清华集训2014

玛里苟斯

statement

\(S\) 是一个可重集合,\(S = \{a_1, a_2, \dots, a_n \}\)

等概率随机取 \(S\) 的一个子集 \(A = \{a_{i_1}, \dots, a_{i_m}\}\)

计算出 \(A\) 中所有元素异或 \(x\), 求 \(x^k\) 的期望。

output

如果结果是整数,直接输出。如果结果是小数(显然这个小数是有限的),输出精确值(末尾不加多余的 \(0\))。

constraints

\(1 \leq n \leq 100000\)\(1 \leq k \leq 5\)\(a_i \geq 0\)。最终答案小于 \(2^{63}\)\(k = 1,2,3,4,5\) 各自占用 20% 的数据。

sol

我们想对每个数拆位,并将 \(k\) 次方通过枚举拆掉。

可得如下推导:

\[\begin{aligned} &\dfrac{1}{2 ^ n}\sum\limits_{S \subseteq \{1, \cdots, n\}} \left( \bigoplus\limits_{x \in S} a_x \right) ^ k \\ =&\dfrac{1}{2 ^ n}\sum\limits_{S \subseteq \{1, \cdots, n\}} \left[ \sum\limits_{i \in \{0, \cdots, 63\}} 2 ^ i \left( \bigoplus\limits_{x \in S} a_x \operatorname{and} 2 ^ i \right) \right] ^ k \\ =&\dfrac{1}{2 ^ n}\sum\limits_{i_1, \cdots, i_k \in \{0, \cdots, 63\}} 2 ^ {\sum\limits_{j = 1} ^ {k} i_j} \sum\limits_{S \subseteq \{1, \cdots, n\}} \prod\limits_{j = 1} ^ {k} \left( \bigoplus\limits_{x \in S} a_x \operatorname{and} 2 ^ {i_j} \right) \end{aligned} \]

我们好奇一个事情,本题输出方式很奇怪,没有取模也没有保留小数,通过 \(O(n2 ^ n)\) 暴力我们发现答案似乎总是整数或小数部分是 \(0.5\)

我们固定一组 \(i\),然后继续观察:

\[\sum\limits_{S \subseteq \{1, \cdots, n\}} \prod\limits_{j = 1} ^ {k} \left( \bigoplus\limits_{x \in S} a_x \operatorname{and} 2 ^ {i_j} \right) \]

如果我们令 \(b_x = \sum\limits_{j = 1} ^ {k} 2 ^ {j - 1} (a_x \operatorname{and} 2 ^ {i_j})\),则上式为:

\[\sum\limits_{S \subseteq \{1, \cdots, n\}} [2 ^ k - 1 = \bigoplus\limits_{x \in S} b_x] \]

翻译以下是问序列 \(b_n\) 有多少个子集的异或和为 \(2 ^ k - 1\),且 \(b_x \in \{0, \cdots, 2 ^ k - 1\}\)

可以通过线性基解决。

线性基

直接的定义可以说是异或高斯消元的结果,就是上三角矩阵。

如果使用异或高斯约旦消元,会得到一条对角线。

但是我们可以简单地动态插入一行维护:

bool insert(T x) {
    for (int i = W - 1; i >= 0; i--) {
        if (x >> i & 1) {
            if (!num[i]) {
                num[i] = x;
                rank++;
                return 1;
            }
            x ^= num[i];
        }
    }
    return 0;
}

考虑如下问题:

求序列 \(a_n\) 有多少个子集异或和为 \(x\)

\(a_i\) 都插入线性基,然后判断 \(x\) 是否与 \(a_n\) 线性相关。

  • 若线性无关,答案为 \(0\)
  • 若线性相关,在基中存在唯一的方法异或出 \(x\),答案为 \(2 ^ {n - rank}\)

回到原问题即:

\[\begin{aligned} &\dfrac{1}{2 ^ n}\sum\limits_{i_1, \cdots, i_k \in \{0, \cdots, 63\}} 2 ^ {\sum\limits_{j = 1} ^ {k} i_j} \sum\limits_{S \subseteq \{1, \cdots, n\}} [2 ^ k - 1 = \bigoplus\limits_{x \in S} b_x] \\ =&\sum\limits_{i_1, \cdots, i_k \in \{0, \cdots, 63\}} [线性相关] 2 ^ {\left( \sum\limits_{j = 1} ^ {k} i_j \right) - rank} \end{aligned} \]

时间复杂度 \(O(nk64 ^ k)\)

优化

首先注意到复杂度里的 \(64\) 其实是可能达不到的,因为答案小于 \(2 ^ {63}\),所以值域很大会导致答案极易爆范围,记最大位数 \(w\),时间复杂度 \(O(nkw ^ k)\)​。

对于 \(x, y \in a\),序列 \((a / \{x, y\}) \cup \{x, x \oplus y\}\) 的子集异或值的总体没有改变,所以每次对 \(n\) 个数求线性基是不必要的,只需在 \(n\) 个数的线性基上求线性基即可,时间复杂度 \(O(kw ^ {k + 1})\)

可以通过。

也可以对 \(k = 1, 2\) 跑上述枚举,\(k = 3, 4, 5\)\(O(w2 ^ w)\) 暴力。

实现

#include <bits/stdc++.h>
using namespace std;
using ull = unsigned long long;
template<class T, const int W>
struct Basis {
    int rank;
    array<T, W> num;
    Basis() : rank{}, num{} {}
    bool insert(T x) {
        for (int i = W - 1; i >= 0 && x; i--) {
            if (x >> i & 1) {
                if (!num[i]) {
                    num[i] = x;
                    rank++;
                    return 1;
                }
                x ^= num[i];
            }
        }
        return 0;
    }
};
constexpr int N = 1e5 + 5;
int n, k, num[7], lim;
ull a[N], ans;
bool flag;
void dfs(int x) {
    if (x == k + 1) {
        Basis<int, 5> basis;
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            int v = 0;
            for (int j = 1; j <= k; j++) {
                v = v * 2 + (a[i] >> num[j] & 1);
            }
            basis.insert(v);
        }
        if (!basis.insert((1 << k) - 1)) {
            int sum = 0;
            for (int i = 1; i <= k; i++) {
                sum += num[i];
            }
            sum -= basis.rank;
            if (sum == -1) {
                ans += flag;
                flag ^= 1;
            } else {
                ans += 1ull << sum;
            }
        }
        return;
    }
    for (int i = 0; i <= lim; i++) {
        num[x] = i;
        dfs(x + 1);
    }
}
int main() {
    cin >> n >> k;
    Basis<ull, 64> basis;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        basis.insert(a[i]);
    }
    n = 0;
    for (int i = 0; i <= 63; i++) {
        if (basis.num[i]) {
            a[++n] = basis.num[i];
            lim = i;
        }
    }
    dfs(1);
    cout << ans;
    if (flag) {
        cout << ".5";
    }
    cout << "\n";
    return 0;
}

主旋律

statement

\(n\) 个点 \(m\) 条边的简单有向图,求有多少边的子集删去之后整个图仍然强联通。

constraints

对于 100% 的数据满足: \(n \leq 15, 0 \leq m \leq n(n − 1)\)

sol

生成函数

注意到强连通分量如果连成 DAG 则就是简单有向图,说明如果 DAG 能用符号化方法说明,那么只需要求 \(\ln\text{简单有向图}\)

留下关于这部分 GF 理论的一篇 paper

DP

本问题是明显的状压,但研究一般的计数得到的 DP 是类似的,不妨从另一些问题入手。

\(n\) 个点形成多少个有标号 DAG。

\(f_i\) 等于 \(n\) 个点的有标号 DAG 数。

枚举 \(j\) 个入度为 \(0\) 的点

\[f_i = \sum\limits_{j = 1} ^ i \binom{i}{j} f_{i - j} 2 ^ {j(i - j)} \]

实际上算出来的有问题,因为要保证剩下 \(i - j\) 都有入度。

我们尝试使用容斥,引入容斥系数 \(g_i\)

\[f_i = \sum\limits_{j = 1} ^ i \binom{i}{j} g_if_{i - j} 2 ^ {j(i - j)} \]

那么对于一个有 \(i\) 个源点的 DAG,会满足

\[\sum\limits_{j = 1} ^ i \binom{i}{j} g_j = 1 \]

\(g_i = (-1) ^ {i + 1}\),容易验证上式成立。

综上所述,可以得到一个 \(O(n ^ 2)\) 的 DP

\[f_i = \sum\limits_{j = 1} ^ i \binom{i}{j} (-1) ^ {i + 1} f_{i - j} 2 ^ {j(i - j)} \]

\(n\) 个点形成多少个弱连通有标号 DAG。

注意到本质是对上一个问题求 \(\ln\)

\(f_i\) 等于 \(n\) 个点的有标号 DAG 数。

\(g_i\) 等于 \(n\) 个点的弱连通有标号 DAG 数。

\[f_i = \sum\limits_{j = 1} ^ i \binom{i}{j} (-1) ^ {i + 1} f_{i - j} 2 ^ {j(i - j)} \]

容斥计算强连通分量数 \(\ge 2\) 的有标号 DAG 数量。

\(h_i\) 等于 \(n\) 个点的有标号有向图数。

\[h_i = 2 ^ {i (i - 1)} \]

枚举 \(1\) 号点所在连通块大小

\[g_i = h_i - \sum\limits_{j = 1} ^ {i - 1} \binom{i - 1}{j - 1} g_j f_{i - j} \]

综上所述,可以得到一个 \(O(n ^ 2)\) 的 DP。

\(n\) 个点形成多少个有标号 SCC。

瓶颈在于容斥系数与 DAG 的 SCC 数量挂钩,所以我们对容斥系数进行 DP。

\(f_i\) 等于 \(n\) 个点的有标号 SCC 数。

\(g_i\) 等于 \(n\) 个点的有标号有向图的容斥系数和,即缩点后奇数个 SCC 算 \(+1\),偶数个 SCC 算 \(-1\)

注意 \(f\)\(g\) 之间类似 \(\ln, \exp\) 的关系,枚举 \(1\) 号点所在连通块大小,并分离 SCC 大小为 \(1\) 的 DAG。

\[\begin{aligned} &g_i = f_i - \sum\limits_{j = 1} ^ {i - 1} \binom{i - 1}{j - 1} f_jg_{i - j} &(*) \end{aligned} \]

\(h_i\) 等于 \(n\) 个点的有标号有向图数。

\[h_i = 2 ^ {i (i - 1)} \]

根据之前的容斥,枚举有向图缩点后 DAG 的源点

\[h_i = \sum\limits_{j = 1} ^ {i} \binom{i}{j} g_j h_{i - j}2 ^ {j (i - j)} \]

分离右边的 \(g_i\)

\[g_i = h_i - \sum\limits_{j = 1} ^ {i - 1} \binom{i}{j} g_j h_{i - j} 2^{j(i - j)} \]

这样可以 \(O(n^2)\)​ DP 出 \(g_i\)

观察 \((*)\) 式,可以求出 \(f\)

\[f_i = g_i + \sum\limits_{j = 1} ^ {i - 1} \binom{i - 1}{j - 1} f_jg_{i - j} \]

综上所述,可以得到一个 \(O(n ^ 2)\) 的 DP。


回到原问题,我们稍微改一改即可。

\(f_s\) 表示 \(s\) 的导出子图的强连通子图数。

\(g_s\) 表示 \(s\) 的导出子图的子图容斥系数和,即缩点后奇数个 SCC 算 \(+1\),偶数个 SCC 算 \(-1\)

\(h_s\) 表示 \(s\) 的导出子图的子图数。

\(\operatorname{cross}(s, t)\) 表示 \(s\) 连向 \(t\) 的边数。

\[h_s = 2 ^ {s \text{ 的导出子图边数}} \]

\[g_s = h_s - \sum\limits_{\varnothing \ne t \subset s} g_t h_{s/t}2 ^ {\operatorname{cross}(t, s / t)} \]

\[f_s = g_s + \sum\limits_{1\in t \subset s} f_tg_{s / t} \]

这和上面一个问题几乎是一样的。

使用 bitset 处理集合的入边和出边,可以快速计算出 \(\operatorname{cross}(t, s / t)\) 的值。

时间复杂度 \(O(m2 ^ n + \dfrac{m3 ^ n}{\omega})\),可以通过。

实现

#include <bits/stdc++.h>
using namespace std;
constexpr int mod = 1e9 + 7;
int add(int a, int b) {
    return a += b, a < mod ? a : a - mod;
}
int sub(int a, int b) {
    return a -= b, a >= 0 ? a : a + mod;
}
int mul(int a, int b) {
    return 1ll * a * b % mod;
}
int inv(int a, int b = mod) {
    return a == 1 ? 1 : b - inv(b % a, a) * b / a;
}
constexpr int S = 1 << 15;
int n, m, pow2[211];
vector<pair<int, int>> E;
int f[S], g[S], h[S];
bitset<210> in[S], out[S];
int cross(int s, int t) {
    return (out[s] & in[t]).count();
}
int main() {
    cin >> n >> m;
    pow2[0] = 1;
    for (int i = 1; i <= m; i++) {
        pow2[i] = mul(pow2[i - 1], 2);
    }
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        E.emplace_back(u, v);
    }
    for (int s = 0; s < 1 << n; s++) {
        for (int i = 0; i < m; i++) {
            auto [u, v] = E[i];
            if (s >> u & 1 && s >> v & 1) {
                h[s]++;
            }
            if (~s >> u & 1 && s >> v & 1) {
                in[s][i] = 1;
            }
            if (s >> u & 1 && ~s >> v & 1) {
                out[s][i] = 1;
            }
        }
        h[s] = pow2[h[s]];
    }
    for (int s = 0; s < 1 << n; s++) {
        g[s] = h[s];
        for (int t = s - 1 & s; t; t = t - 1 & s) {
            g[s] = sub(g[s], mul(mul(g[t], h[s ^ t]), pow2[cross(t, s ^ t)]));
        }
        f[s] = g[s];
        for (int t = s - 1 & s; t; t = t - 1 & s) {
            if (t & 1) {
                f[s] = add(f[s], mul(f[t], g[s ^ t]));
            }
        }
    }
    cout << f[(1 << n) - 1] << "\n";
    return 0;
}

奇数国

statement

序列 \(a\)\(n\) 个数,初始为 \(3\)

\(q\) 次操作,有两种:

  1. \(\varphi\left( \prod\limits_{i = l} ^ r a_i \right)\)
  2. \(a_x \leftarrow y\)

保证序列所有数的质因子始终只可能是最小的 \(60\) 个素数 \((2, 3, 5, \cdots, 281)\)

constraints

对于 100% 的数据满足: \(n, q \leq 10 ^ 5, a_i, y \le 10 ^ 6\)​。

sol

因为

\[\varphi (x) = x \prod\limits_{p \text{是质数}} \dfrac{p - 1}{p} \]

所以维护乘积和每个素数的出现情况即可,维护后者可以压位。

时间复杂度 \(O(n\log n)\)

实现

#pragma GCC optimize("Ofast", "inline")
#include <bits/stdc++.h>
#define ls p << 1
#define rs ls | 1
#define mid ((l + r) >> 1)
using namespace std;
struct IO {
    template<class T>
    IO &operator>>(T &x) {
        x = 0;
        int f = 1;
        char c;
        while (!isdigit(c = getchar())) {
            if (c == '-') {
                f = -1;
            }
        }
        do {
            x = x * 10 + c - '0';
        } while (isdigit(c = getchar()));
        x *= f;
        return *this;
    }
    IO &operator>>(char &x) {
        while (!isalpha(x = getchar()));
        return *this;
    }
    template<class T>
    IO &operator<<(T x) {
        static int s[130], t = 0;
        if (x < 0) {
            x = -x;
            putchar('-');
        }
        do {
            s[++t] = x % 10;
        } while (x /= 10);
        while (t) {
            putchar('0' + s[t--]);
        }
        return *this;
    }
    IO &operator<<(char x) {
        putchar(x);
        return *this;
    }
} io;
constexpr int mod = 19961993;
int add(int a, int b, int P = mod) {
    a += b;
    if (a >= P) {
        a -= P;
    }
    return a;
}
int sub(int a, int b, int P = mod) {
    a -= b;
    if (a < 0) {
        a += P;
    }
    return a;
}
int mul(int a, int b, int P = mod) {
    return 1ll * a * b % P;
}
int inv(int a, int P = mod) {
    if (a == 1) {
        return 1;
    } else {
        return P - 1ll * inv(P % a, a) * P / a;
    }
}
int divi(int a, int b, int P = mod) {
    return mul(a, inv(b, P), P);
}
int qpow(int a, int b, int P = mod) {
    int c = 1;
    while (b) {
        if (b & 1) {
            c = mul(c, a, P);
        }
        a = mul(a, a, P);
        b >>= 1;
    }
    return c;
}
constexpr int N = 1e5 + 5, M = N * 4;
constexpr int pri[61] = {0, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281};
int n = 1e5, q, a[N];
int v[M];
bitset<61> exist[M];
void pull(int p) {
    v[p] = mul(v[ls], v[rs]);
    exist[p] = exist[ls] | exist[rs];
}
void build(int p = 1, int l = 1, int r = n) {
    if (l == r) {
        v[p] = a[l] % mod;
        for (int i = 1; i <= 60; i++) {
            exist[p][i] = a[l] % pri[i] == 0;
        }
        return;
    }
    build(ls, l, mid);
    build(rs, mid + 1, r);
    pull(p);
}
void change(int x, int y, int p = 1, int l = 1, int r = n) {
    if (l == r) {
        a[l] = y;
        v[p] = a[l] % mod;
        for (int i = 1; i <= 60; i++) {
            exist[p][i] = a[l] % pri[i] == 0;
        }
        return;
    }
    if (x <= mid) {
        change(x, y, ls, l, mid);
    } else {
        change(x, y, rs, mid + 1, r);
    }
    pull(p);
}
int askv(int x, int y, int p = 1, int l = 1, int r = n) {
    if (x <= l && r <= y) {
        return v[p];
    }
    int ans = 1;
    if (x <= mid) {
        ans = mul(ans, askv(x, y, ls, l, mid));
    }
    if (mid < y) {
        ans = mul(ans, askv(x, y, rs, mid + 1, r));
    }
    return ans;
}
bitset<61> askexist(int x, int y, int p = 1, int l = 1, int r = n) {
    if (x <= l && r <= y) {
        return exist[p];
    }
    bitset<61> ans;
    if (x <= mid) {
        ans |= askexist(x, y, ls, l, mid);
    }
    if (mid < y) {
        ans |= askexist(x, y, rs, mid + 1, r);
    }
    return ans;
}
int main() {
    for (int i = 1; i <= n; i++) {
        a[i] = 3;
    }
    build();
    io >> q;
    while (q--) {
        int op, x, y;
        io >> op >> x >> y;
        if (op == 0) {
            int v = askv(x, y);
            auto exist = askexist(x, y);
            for (int i = 1; i <= 60; i++) {
                if (exist[i]) {
                    v = mul(v, pri[i] - 1);
                    v = divi(v, pri[i]);
                }
            }
            io << v << '\n';
        } else {
            change(x, y);
        }
    }
    return 0;
}

posted @ 2023-08-06 19:07  olriutre  阅读(20)  评论(0)    收藏  举报