Atcoder泛做 2

Atcoder泛做-2

ABL_F.Heights and Pairs

有$2N$个数$h_i$,两两分成一组,形成$N$个数对。问有多少种分组的方案使得每组中两个元素互不相同

$N\leq 50000, h_i\leq 100000$

计数问题可以先从集合的角度考虑。

题目最终所求为恰好有0组数存在相同的方案数,不妨设集合幂级数$f[S]$表示将$S$中相同的数放到一组的方案数,其中$S$是一个长度为$2N$的二进制串,所求即为$f[\phi]$。

考虑放宽限制,即向超集方向反演。设$g[S]=\sum_{S \subset T} f[T]$,根据反演公式有$f[S]=\sum_{S \subset T} (-1)^{|T|-|S|} g[T]$。

计算$g[S]$即为钦定若干组相同的数,剩下的数随意的方案。可以发现,反演的系数以及“剩下的数随意”的方案都和集合$S$实际长度有关,于是考虑用长度来压缩集合。

设$h[i]=\sum_{|S|=i} g[S]$,我们发现所有$g[S]$中随意的方案都是$\frac{(2N-2i)!}{(N-i)!2^{N-i}}$。提取这个公因数之后,$h[i]=L(i)*\frac{(2N-2i)!}{(N-i)!2^{N-i}}$,其中$L(i)$表示选出$i$对数相同的方案数。

似乎可以DP。

设$cnt[i]$为数值为$i$一共有多少个。我们对每种数值枚举其分出的数对$j$做决策,可以写出一个背包状DP。

考虑到数据范围过大,因此这个背包DP使用OGF来加速。

设$OGF(i) = \sum_{j=0}^{2j\leq cnt[i]} C_{cnt[i]}^{2j}\frac{(2j)!}{j!2^j}x^j$

由于$cnt[i]$的总和不超过$2n$,因此所有的OGF一共不超过$n$项。但是$i$的范围是$10^5$。

考虑用分治FFT加速,这样每一层计算的多项式总项数不超过$n$,一共$log_2 (max{h_i})$层。

最终根据反演公式得到$f[\phi]$。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

constexpr int N = 1e5;

int Main()
{
    int n;
    read(n);
    std::vector<int> h(n + n + 1);
    std::vector<int> cnt(N + 1);
    for (int i = 1; i <= n + n; ++i)
        read(h[i]), cnt[h[i]]++;

    Combinatorics<modint> combinatorics(n + n + 1);
    std::vector<modint> inv2(n + 1);
    inv2[0] = 1;
    inv2[1] = modint{1} / 2;
    for (int i = 2; i <= n; ++i)
        inv2[i] = inv2[i - 1] * inv2[1];

    std::vector<Poly<modint>> F(N + 1);

    auto all = [&](int x) -> modint
    {
        return combinatorics.fac[x + x] * combinatorics.invfac[x] * inv2[x];
    };

    for (int i = 0; i <= N; ++i)
    {
        auto &p = F[i];
        p.a.assign(cnt[i] / 2 + 1, modint{0});
        for (int j = 0; j <= cnt[i] / 2; ++j)
            p[j] = combinatorics(cnt[i], j + j) * all(j);
    }

    auto solve = [&](auto &&self, int l, int r) -> Poly<modint>
    {
        if (l == r)
            return F[l];

        int mid = (l + r) >> 1;
        Poly G = self(self, l, mid), H = self(self, mid + 1, r);
        return G * H;
    };

    auto G = solve(solve, 0, N);
    G.resize(n + 1);

    modint phi = 0;
    for (int i = 0; i <= n; ++i)
    {
        if (i % 2 == 0)
            phi += G[i] * all(n - i);
        else
            phi -= G[i] * all(n - i);
    }

    printf("%d\n", phi);

    return 0;
}

总结一下:炫酷反演魔术以及用长度“压缩”集合状态的技巧。实际上所谓的容斥以及容斥系数,不妨直接考虑用反演来做,对于我个人来说是更简单更清晰的。

APC001F.XOR Tree

一棵$N$个节点的无向树,有边权。每次可以选择一条路径,将整条路径上所有的边权都异或一个$x$。

请找到最小的使得所有边权为0的操作次数。

树上路径的边权异或操作,可以考虑转化边权为点权。因此路径上除了两端之外,一个点周围会有两条边被异或了同一个数,如果这个点的点权跟这两条边有关,那么相当于点权只有两端在变动。

那么我们新构造的点权,既要考虑到儿子的边,也要考虑到父亲的边,不妨设点权为所有边的异或和。

于是边权全为0的充要条件是点权全为0。

充分条件是显然的,必要条件的话我们从叶子开始往上推,这样儿子边全为0,那么父亲边也只能为0。

那么一次操作就变成了操作路径两端的点权。显然一开始我们可以把点权相同的若干点放到一起操作,这样每种点权最终至多剩下一个。

现在我们有若干非零点权(不超过15个),每种至多一个,问最少用几次操作可以使得他们都为0。

考虑有$k$个数,$a_1, …, a_k$,如果$a_1\oplus\cdots \oplus a^k=0$,那么我们可以用$k-1$次操作使他们全变成0。

所以这个问题变成了一个分组问题,把这些权值分成若干组,每组的收益是1,最大化收益。

因此用状压DP解决,总复杂度为$O(3^{15}+N)$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

int Main()
{
    int n;
    read(n);

    std::vector<int> a(n + 1);
    for (int i = 1; i < n; ++i)
    {
        int u, v, w;
        read(u, v, w);
        u++, v++;
        a[u] ^= w, a[v] ^= w;
    }

    int ans = 0;
    std::array<int, 16> cnt{};
    for (int i = 1; i <= n; ++i)
        cnt[a[i]]++;
    for (int i = 1; i < 16; ++i)
        ans += cnt[i] / 2, cnt[i] %= 2;

    int all = 0;
    for (int i = 1; i < 16; ++i)
        if (cnt[i])
            all |= (1 << i);
    // printf("%d\n", all);
    std::vector<int> dp(1 << 16);
    for (uint mask = 1; mask < (1 << 16); ++mask)
    {
        dp[mask] = INF<int>;
        if ((mask & all) != mask)
            continue;
        int sum = 0;
        for (int i = 0; i < 16; ++i)
            if (mask & (1 << i))
                sum ^= i;
        dp[mask] = sum == 0 ? std::popcount(mask) - 1 : INF<int>;
    }

    for (uint mask = 1; mask < (1 << 16); ++mask)
    {
        if ((mask & all) != mask)
            continue;
        for (int pmask = mask; pmask; pmask = (pmask - 1) & mask)
            dp[mask] = std::min(dp[mask ^ pmask] + dp[pmask], dp[mask]);
    }

    ans += dp[all];
    std::cout << ans << std::endl;

    return 0;
}

总结一下:前半部分是相对来说有点套路的树上路径异或问题;然后到每种点权只有一个的时候,稍微有点不知所措,这个时候可以考虑用套路化不那么明显的算法尝试着去套,比如DP、网络流等等。

ABC400G.Patisserie ABC 3

有三个长度为$N$的数组$a,b,c$。现在选出$k$个匹配,$(i,j)$匹配的权值是$max(a_i+a_j,b_i+b_j,c_i+c_j)$。求$k$个匹配的最大权

首先这个匹配的权值是忽悠人的。因为总权值是加法,单个权值还是加法中取MAX,因此我们把MAX打开,这样相当于选出$2k$个数,每个数四种选法(不选,选$a$,选$b$,选$c$),每种选出来的都得是偶数个。这样虽然统计了不合法的策略,但是最佳的策略一定合法且一定能被统计到。

考虑DP,$dp[i][k][mask]$表示前$i$个数,选出$k$个,目前$a,b,c$的数量奇偶性是$mask$的最大权值。转移是很简单的。

复杂度$O(NK)$

根据KM算法或者费用流我们可以知道最大权值匹配这个东西是个凸的。

不妨考虑Alien DP。因为选出$k$个匹配是凸的,利用Alien DP我们可以得到分一组额外扣除$mid$的权值,那么放到每个数上就是额外扣除$mid/2$的权值。

为了避免$mid/2$带来的浮点数,不妨考虑将权值双倍,这样单个数损失$mid$,但权值是$2a[i],2b[i],2c[i]$

这样总复杂度$O(Nlog_2N)$,有一个32的常数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int Main()
{
    int n, k;
    std::cin >> n >> k;
    std::vector<std::array<int, 3>> a(n + 1);
    for (int i = 1; i <= n; ++i)
        for (int p = 0; p < 3; ++p)
            std::cin >> a[i][p];

    // std::vector<std::array<std::pair<ll, int>, 8>> dp(n + 1);

    auto DP = [&](auto mid)
    {
        std::vector<std::pair<ll, int>> dp(8), ndp(8);
        std::fill(dp.begin(), dp.end(), std::pair{(ll)-1e18, 0});

        dp[0] = {0, 0};

        for (int i = 1; i <= n; ++i)
        {
            ndp = dp;
            for (int mask = 0; mask <= 7; ++mask)
            {
                for (int p = 0; p < 3; ++p)
                    ndp[mask] = std::max(ndp[mask], {dp[mask ^ (1 << p)].first + 2 * a[i][p] - mid, dp[mask ^ (1 << p)].second + 1});
            }
            dp = ndp;
        }

        return dp[0];
    };

    ll l = -1e10, r = 1e10;
    while (l <= r)
    {
        auto mid = (l + r) >> 1;

        auto res = DP(mid);
        if (res.second >= k + k)
            l = mid + 1;
        else
            r = mid - 1;
    }

    // printf("%d\n", l - 1);
    auto res = DP(l - 1);
    auto ans = res.first + 1ll * (k + k) * (l - 1);
    printf("%lld\n", ans >> 1);

    return 0;
}
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus