状压 DP
2025-08-05

没补完(1/3),动作还是太慢了


A - 只不过是长的领带 2 / Just Long Neckties 2

https://www.luogu.com.cn/problem/P11665

需要观察到,任意时刻 \(B\) 中不存在重复元素。把 \(B\) 压出来,令 \(f_{i,S}\) 表示选了 \(i\),当前 \(B\)\(S\) 是否可行,能够 \(O(n\cdot 2^V)\)。对于某个 \(S\),只关心它最远能到达哪个 \(i\),故令 \(f_S\) 记录之。对于每一个 \(S\),都可以找到 \(f_S\) 后第一对 \(a_i,a_{i+1}\notin S\),用其刷表。

发现『找 \(f_S\) 后第一对非法元素』是很慢的,考虑优化;对于每个 \(i\) 维护 \(p\) 在其后第一次出现的位置 \(x\),对于每个 \(a_x=p\) 维护 \(x\) 后方 \(a_y=p,a_{y+1}=q\) 第一次出现的位置,相当于先找 \(p\) 再找 \((p,q)\),就可以做到 \(O(V^2\cdot 2^V+n\cdot V)\)

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n;
    std::cin >> n;
    std::vector<int> a(n + 1);
    int V = 0;
    for (int i = 1; i <= n; ++i) {
        std::cin >> a[i];
        V = std::max(V, a[i]--);
    }
    std::vector<std::vector<int> > tx(n + 1, std::vector<int> (V)), ty(n + 1, std::vector<int> (V));
    std::vector<int> lp(V);
    std::vector<std::vector<int> > lpq(V, std::vector<int> (V));
    for (int i = n; ~i; --i) {
        if (i && i != n)
            lpq[a[i]][a[i + 1]] = i;
        for (int j = 0; j < V; ++j) {
            tx[i][j] = lp[j];
            if (i != n)
                ty[i][j] = lpq[a[i]][j];
        }
        if (i != 0)
            lp[a[i]] = i;
    }
    int siz = 1 << V, res = 21;
    std::vector<int> f(siz);
    for (int i = 0; i < siz; ++i) {
        std::vector<int> p0, p1;
        for (int j = 0; j < V; ++j)
            if ((i >> j) & 1)
                p1.push_back(j);
            else
                p0.push_back(j);
        int j = n;
        for (auto p : p0)
            if (tx[f[i]][p])
                for (auto q : p0)
                    if (ty[tx[f[i]][p]][q])
                        j = std::min(j, ty[tx[f[i]][p]][q]);
        if (j != n) {
            f[i ^ (1 << a[j])] = std::max(f[i ^ (1 << a[j])], j);
            f[i ^ (1 << a[j + 1])] = std::max(f[i ^ (1 << a[j + 1])], j + 1);
            for (auto k : p1) {
                f[i ^ (1 << k) ^ (1 << a[j])] = std::max(f[i ^ (1 << k) ^ (1 << a[j])], j);
                f[i ^ (1 << k) ^ (1 << a[j + 1])] = std::max(f[i ^ (1 << k) ^ (1 << a[j + 1])], j + 1);
            }
        }
        else
            res = std::min(res, __builtin_popcount(i));
    }
    std::cout << res << '\n';
    return 0;
}

B - Cut and Reorder

https://atcoder.jp/contests/abc328/tasks/abc328_g

不妨先重排再修改,令 \(f_{i,S}\) 表示已经重排好新序列的前 \(i\) 个元素,对应原序列状态 \(S\) 的最小代价。枚举新区间容易转移。可以发现枚举 \(i,S\) 的实际复杂度为 \(O(2^n)\)(空间也可以这么优化),预处理之后总时间复杂度 \(O(n^2\cdot 2^n)\),跑不满,可以通过。

#include <bits/stdc++.h>
const long long inf = 1e18;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n;
    long long c;
    std::cin >> n >> c;
    std::vector<long long> a(n), b(n);
    for (int i = 0; i < n; ++i)
        std::cin >> a[i];
    for (int i = 0; i < n; ++i)
        std::cin >> b[i];
    using arr = std::vector<long long>;
    using brr = std::vector<arr>;
    using crr = std::vector<brr>;
    brr p(n, arr(n));
    crr g(n, brr(n, arr(n)));
    for (int l = 0; l < n; ++l)
        for (int r = l; r < n; ++r) {
            for (int k = l; k <= r; ++k)
                p[l][r] ^= (1 << k);
            for (int R = r - l; R < n; ++R)
                for (int L = R, j = r; j >= l; --L, --j)
                    g[l][r][R] += std::abs(b[L] - a[j]);
        }
    int siz = 1 << n;
    std::vector<long long> f(siz, inf);
    f[0] = 0ll;
    for (int j = 1; j < siz; ++j) {
        int i = __builtin_popcount(j) - 1;
        for (int r = 0; r < n; ++r)
            for (int l = r; ~l && ((j >> l) & 1); --l)
                f[j] = std::min(f[j], f[j ^ p[l][r]] + g[l][r][i] + c);
    }
    std::cout << f[siz - 1] - c << '\n';
    return 0;
}

C - Electric Circuit

https://atcoder.jp/contests/abc321/tasks/abc321_g

很像无向图容斥?令 \(f_S\) 表示让 \(S\) 内部完成配对,可以不连通的方案数,那么当且仅当 \(S\) 内部点出、入度之和相等(记为 \(cnt\))时,\(f_S\) 有值 \(cnt!\)。相似地,设 \(g_S\) 表示让 \(S\) 完成配对,成为一个连通块的方案数,得到 \(g_S=f_S-\sum\limits_{v\in S} f_{S\oplus v}\cdot g_v\)。让 \(v\) 必须包含 \(S\) 中编号最小的点就可以去重。

从贡献的角度出发,\(S\) 带来的贡献就是 \(g_S\cdot f_{U\oplus S}\),其中 \(U\) 是全集。最后除以 \(M!\) 求出期望。

复杂度 \(O(3^n)\)

#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<int> ci(n), co(n);
    for (int i = 1, x; i <= m; ++i)
        std::cin >> x, ++ci[x - 1];
    for (int i = 1, x; i <= m; ++i)
        std::cin >> x, ++co[x - 1];
    std::vector<long long> fac(m + 1);
    fac[0] = 1ll;
    for (int i = 1; i <= m; ++i)
        fac[i] = fac[i - 1] * i % mod;
    auto qkp = [&](long long x, int y) {
        auto res(1ll);
        for (; y; (x *= x) %= mod, y >>= 1)
            if (y & 1)
                (res *= x) %= mod;
        return res;
    };
    int siz = 1 << n;
    std::vector<long long> f(siz), g(siz);
    for (int i = 1; i < siz; ++i) {
        int si = 0, so = 0;
        for (int j = 0; j < n; ++j)
            if ((i >> j) & 1)
                si += ci[j], so += co[j];
        if (si == so)
            f[i] = fac[si];
    }
    auto res(0ll);
    for (int i = 1; i < siz; ++i) {
        g[i] = f[i];
        int mn = 0;
        for (int j = 0; j < n; ++j)
            if ((i >> j) & 1) {
                mn = j;
                break;
            }
        int s = i ^ (1 << mn);
        (g[i] += mod - g[1 << mn] * f[s] % mod) %= mod;
        for (int j = (s - 1) & s; j; j = (j - 1) & s)
            (g[i] += mod - g[j ^ (1 << mn)] * f[s ^ j] % mod) %= mod;
        if (i != siz - 1)
            (res += g[i] * f[(siz - 1) ^ i]) %= mod;
        else
            (res += g[i]) %= mod;
    }
    std::cout << res * qkp(fac[m], mod - 2) % mod << '\n';
    return 0;
}

D - Count Grid 3-coloring

https://atcoder.jp/contests/abc379/tasks/abc379_g

轮廓线 DP。把每一列已经确定的最靠下的元素压起来,每行逐个确定即可。

发现有效状态中只能容许最多一对相邻相同元素,这样复杂度就能降下来了。注意特判 \(1\times 1\) 的情况。

#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::array<int, 15> p;
    p[0] = 1;
    for (int i = 1; i < 15; ++i)
        p[i] = p[i - 1] * 3;
    std::vector<std::vector<int> > a;
    if (n >= m) {
        a.assign(n + 1, std::vector<int> (m + 1));
        for (int i = 1; i <= n; ++i) 
            for (int j = 1; j <= m; ++j) {
                char t;
                std::cin >> t, a[i][j] = (t == '?' ? -1 : t - '1');
            }
    }
    else {
        std::swap(n, m);
        a.assign(n + 1, std::vector<int> (m + 1));
        for (int i = 1; i <= m; ++i)
            for (int j = 1; j <= n; ++j) {
                char t;
                std::cin >> t, a[j][i] = (t == '?' ? -1 : t - '1');
            }
    }
    int siz = p[m];
    using arr = std::vector<long long>;
    using brr = std::vector<arr>;
    using crr = std::vector<brr>;
    std::vector<int> s, tab(siz, -1);
    auto getv_1 = [&](int j, int i) {
        return (j / p[i - 1]) % 3;
    };
    auto getv = [&](int j, int i) {
        return (s[j] / p[i - 1]) % 3;
    };
    auto chg = [&](int j, int i, int v) {
        return tab[s[j] - p[i - 1] * getv(j, i) + p[i - 1] * v];
    };
    auto out = [&](int i) {
        std::string s;
        for (int j = 1; j <= m; ++j)
            s += '1' + getv_1(i, j);
        return s.c_str();
    };
    for (int i = 0; i < siz; ++i) {
        s.push_back(i);
        int cnt = 0;
        for (int j = 2; j <= m; ++j)
            if (getv_1(i, j - 1) == getv_1(i, j) && ++cnt >= 2) {
                s.pop_back();
                break;
            }
    }
    for (int i = 0; i < (int)s.size(); ++i)
        tab[s[i]] = i;
    siz = (int)s.size();
    if (n == 1) {
        std::cout << (a[1][1] == -1 ? 3 : 1) << '\n';
        return 0;
    }
    crr f(n + 1, brr(m + 1, arr(siz)));
    for (int i = 0; i < siz; ++i)
        if ([&](int i, int s) {
            for (int j = 1; j <= m; ++j) {
                if (a[i][j] != -1 && a[i][j] != getv(s, j))
                    return false;
                if (j != 1 && getv(s, j) == getv(s, j - 1))
                    return false;
            }
            return true;
        } (1, i)) {
            for (int p = 0; p < 3; ++p)
                if ((a[2][1] == -1 || a[2][1] == p) && getv(i, 1) != p && ~chg(i, 1, p))
                    (++f[2][1][chg(i, 1, p)]) %= mod;
        }
    for (int i = 2; i <= n; ++i) {
        for (int k = 1; k < m; ++k)
            for (int j = 0; j < siz; ++j)
                if (f[i][k][j]) {
                    // printf("f[%d][%d][%s] = %lld\n", i, k, out(s[j]), f[i][k][j]);
                    for (int p = 0; p < 3; ++p)
                        if ((a[i][k + 1] == -1 || a[i][k + 1] == p) && getv(j, k) != p && getv(j, k + 1) != p && ~chg(j, k + 1, p))
                            (f[i][k + 1][chg(j, k + 1, p)] += f[i][k][j]) %= mod;
                }
        for (int j = 0; j < siz; ++j)
            if (i != n && f[i][m][j])
                for (int p = 0; p < 3; ++p)
                    if ((a[i + 1][1] == -1 || a[i + 1][1] == p) && getv(j, 1) != p && ~chg(j, 1, p))
                        (f[i + 1][1][chg(j, 1, p)] += f[i][m][j]) %= mod;
    }
    auto res = 0ll;
    for (int i = 0; i < siz; ++i)
        (res += f[n][m][i]) %= mod;
    std::cout << res << '\n';
    return 0;
}

E - Pure Straight

https://atcoder.jp/contests/arc126/tasks/arc126_d

手玩发现只要最终序列确定,那么移动的顺序不影响答案。故考虑确定目标位置和移动序列。考虑绝对值的几何意义,不妨令目标子序列中元素集中到被选中位置的中间元素,此时的代价可以计算。用点二进制技巧和库函数可以 \(O(1)\) 算代价。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, k;
    std::cin >> n >> k;
    int siz = 1 << k;
    std::vector<int> a(n + 1), f(siz, 0x3f3f3f3f);
    auto out = [&](int j) {
        std::string s;
        for (int i = 0; i < k; ++i)
            s += ('0' + ((j >> i) & 1));
        return s.c_str();
    };
    f[0] = 0;
    for (int i = 1; i <= n; ++i) {
        std::cin >> a[i], --a[i];
        for (int j = siz - 1; j >= 0; --j) {
            if (!((j >> a[i]) & 1))
                f[j | (1 << a[i])] = std::min(f[j | (1 << a[i])], f[j] + __builtin_popcount(j & (~((1 << a[i]) - 1))));
            f[j] += std::min(__builtin_popcount(j), k - __builtin_popcount(j));
        }
    }
    std::cout << f[siz - 1] << '\n';
    return 0;
}

F - 123 Set

https://atcoder.jp/contests/arc184/tasks/arc184_b

做过 集合选数(这个 trick 见过很多次了,应该不只这一道,但我想不起来了)很容易想到画一个表格或者 DAG 出来,其实都能做

对于表格左上角和 DAG 的源点,肯定是一个 \(x\),其不是 \(2\)\(3\) 的倍数。如果画表,横乘 3 竖乘 2,观察一下是包含了 \(1\sim n\) 恰好一次的很多个杨表(没什么用,提一嘴而已),考虑转化目标,发现是用一个不可旋转的倒 L 形骨牌可叠放地铺满异形表格,可以考虑轮廓线 DP

具体地,用 1 来表示拐角处,0 表示其他,叠放的时候 1 的优先级比 0 高,然后就可以做了。以 3 为行,悲观估计单个表格大概有 \(31\times 2^{19}\) 个状态,运算次数差不多 \(31\times 19\times 2^{19}\);再发现长得一模一样的表格肯定方案数是一样的,如果把任意一个表格全部除以 \(x\),就会得到 \(n=10^9\div x\)\(1\) 为左上角的杨表,就是说长相只和 \(10^9\div x\) 的值有关,可以整除分块 😱 可预计的跑得非常不满,实践下来是可以过的(但是很慢)

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n;
    std::cin >> n;
    auto calc = [&](int r) {
        return r - r / 2 - r / 3 + r / 6;
    };
    auto dp = [&](int lim) {
        if (lim == 1) {
            // printf("lim = 1, ret 1");
            return 1;
        }
        int n = 1, m = 1;
        for (int k = 1; k * 2 <= lim; ++n, k *= 2);
        for (int k = 1; k * 3 <= lim; ++m, k *= 3);
        int siz = 1 << m;
        std::vector<std::vector<std::vector<int> > > f(2, std::vector<std::vector<int> > (2, std::vector<int> (siz))), p(n + 1, std::vector<std::vector<int> > (m));
        std::vector<std::vector<std::vector<std::pair<int, int> > > > t(2, std::vector<std::vector<std::pair<int, int> > > (2, std::vector<std::pair<int, int> > (siz))); // sb
        auto upd = [&](int i, int k, int j, int v) {
            if (t[i & 1][k & 1][j] != std::make_pair(i, k)) {
                p[i][k].push_back(j);
                t[i & 1][k & 1][j] = { i, k }, f[i & 1][k & 1][j] = v;
            }
            else
                f[i & 1][k & 1][j] = std::min(f[i & 1][k & 1][j], v);
            return;
        };
        auto chg = [&](int i, int j, int v) {
            return i ^ (((i >> j) & 1) << j) ^ (v << j);
        };
        for (int i = 0; i < siz; ++i) {
            bool flag = 0;
            for (int j = 0; j < m; ++j)
                if (((i >> j) & 1) || (j && ((i >> (j - 1)) & 1)));
                else {
                    flag = 1;
                    break;
                }
            if (!flag)
                upd(1, m - 1, i, __builtin_popcount(i));
        }
        for (int i = 2; i <= n; i++) {
            int m1 = 1;
            for (int x = (1 << (i - 1)); x * 3ll <= lim; ++m1, x *= 3);
            int siz1 = 1 << m1;
            for (auto j : p[i - 1][m - 1]) {
                if (j & 1)
                    upd(i, 0, chg(j & (siz1 - 1), 0, 0), f[(i - 1) & 1][(m - 1) & 1][j]);
                upd(i, 0, chg(j & (siz1 - 1), 0, 1), f[(i - 1) & 1][(m - 1) & 1][j] + 1);
            }
            m = m1, siz = siz1;
            for (int k = 0; k < m - 1; ++k)
                for (auto j : p[i][k]) {
                    if (((j >> k) & 1) || ((j >> (k + 1)) & 1))
                        upd(i, k + 1, chg(j, k + 1, 0), f[i & 1][k & 1][j]);
                    upd(i, k + 1, chg(j, k + 1, 1), f[i & 1][k & 1][j] + 1);
                }
        }
        int res = 0x3f3f3f3f;
        for (auto i : p[n][m - 1])
            res = std::min(res, f[n & 1][(m - 1) & 1][i]);
        return res;
    };
    int res = 0;
    for (int l = 1, r; l <= n; l = r + 1) {
        r = n / (n / l);
        if (calc(r) - calc(l - 1))
            res += (calc(r) - calc(l - 1)) * dp(n / l);
    }
    std::cout << res << '\n';
    return 0;
}

言论