DP

T1

CF1383E Strange Operation

考虑对于判定过程计数。给你一个 \(01\)\(T\),判断它是否合法,我们考虑从前往后贪心。设当前原串匹配到第 \(i\) 位,\(T\) 匹配到第 \(j\) 位。若 \(T\) 接下来是一个 \(1\),则我们找到 \(S\)\(i\) 之后的第一个 \(1\) 匹配。否则设 \(T_j\) 所在 \(0\) 连续段长度为 \(p\),由于操作的性质,任何 \(0\) 连续段不会变长,则我们需要找到 \(S_i\) 后面第一个长度 \(\ge p\)\(0\) 连续段,跳到它的开头,然后把两边的 \(0\) 连续段直接都匹配掉。

但是这样描述 \(0\) 连续段显然不好 dp,我们得按顺序来。于是若当前我们想要放一个 \(0\)\(S_{i + 1} \ne 0\),设 \(S_i\) 所在 \(0\) 连续段长为 \(q\),我们就跳到 \(S_{i + 1}\) 后面第一个长度 \(\ge q\)\(0\) 连续段,跳到它的第 \(q\) 位继续匹配。这个过程可以用单调栈预处理。然后我们最好钦定 \(T\) 的开头结尾都是 \(1\),最后再乘上 \(S\) 的前缀 \(0\) 和后缀 \(0\),这样更方便。

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 1000000007;
inline void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
string str;
int n;
int f[1000005], ap[2];
int nxt[1000005][2];
int stk[1000005], sz;
int d[1000005];
signed main() {
    cin >> str, n = str.size();
    str = ' ' + str;
    ap[0] = ap[1] = n + 1;
    for (int i = n; ~i; i--) {
        nxt[i][0] = ap[0];
        nxt[i][1] = ap[1];
        if (i) 
            ap[str[i] - '0'] = i;
    }
    if (ap[1] == n + 1) {
        cout << n << "\n";
        return 0;
    }
    for (int i = n; i; i--) {
        if (str[i] == '0' && str[i - 1] != '0') {
            d[i] = nxt[i][1] - i;
            while (sz && d[stk[sz]] <= d[i]) --sz;
            if (!sz) 
                nxt[nxt[i][1] - 1][0] = n + 1;
            else 
                nxt[nxt[i][1] - 1][0] = stk[sz] + nxt[i][1] - i;
            stk[++sz] = i;
        }
    }
    f[ap[1]] = 1;
    int ans = 0;
    for (int i = ap[1]; i < n; i++) {
        Madd(f[nxt[i][0]], f[i]);
        Madd(f[nxt[i][1]], f[i]);
    }
    int cnt = 1;
    for (int i = n; str[i] == '0'; --i) ++cnt;
    for (int i = 1; i <= n; i++) {
        if (str[i] == '1') 
            Madd(ans, f[i]);
    }
    cout << ans * nxt[0][1] % P * cnt % P << "\n";
    return 0;
}

T2

AT_joisc2016_c Solitaire

首先发现每一个连通块都是独立的,对每个连通块分别处理,最后组合数合并。每个连通块一定是中间一行全部没填,然后上下有一些位置没填。我们考虑按列 dp,按顺序考虑每个中间位置。对于每个中心位置,我们钦定它如果能用列放置就用列放置,否则用行放置。设 \(f_{i, 0 / 1}\) 表示前 \(i\) 列,最后一列的中心位置是行还是列放置。由于有的时候需要保证中心位置能进行行放置,我们还需要知道相邻两列中心位置放置的顺序。因此需要再加一位表示最后一列的中心位置是在所有空格里第几个放置的。\(f_{i, j, 0 / 1}\) 表示考虑了前 \(i\) 列,第 \(i\) 列的中心位置是所有空格里第 \(j\) 个放置的,用的是行放置(\(0\))还是列放置(\(1\))。

然后考虑转移,分三种情况讨论:

  1. \(1 \rightarrow 1\):只需要保证在放第 \(i\) 列的中心格时其上下两个都已经放好。

  2. \(0 \rightarrow 1\):需要保证第 \(i\) 列的中心格在第 \(i - 1\) 列的中心格之前放置,且第 \(i\) 列的上下两格在中心格放置之前被放置。

  3. \(1 \rightarrow 0\):需要保证第 \(i\) 列的中心格在第 \(i - 1\) 列的中心格之后放置,且第 \(i\) 列的上下两格不能都在中心格之前被放置。

三种情况分别可以列出方程,其中第三种较复杂。三种情况都可以使用前缀和优化转移,于是复杂度就是 \(\mathcal{O}(n^2)\)。注意如果连通块的第一列是棋盘的第一列,则这一列不能用行放置。棋盘最后一列同理。

关于无解的情况,要么是原棋盘上的四角有空格,要么是第一三行存在空个构成的长度 \(\ge 2\) 的连续段,其他情况都有解。一三行上的孤立空格可以任意顺序放,阶乘一下用组合数插到总方案中即可。

代码
#include <iostream>
#include <string.h>
#define int long long
using namespace std;
const int P = 1000000007;
inline void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
int n;
string str[3];
int f[2005][6005][2]; // 1 : column 0 : row
int C[6005][6005], fac[6005];
int p[6010][3];
int cnt;
int work(int l, int r) {
    // use column if can
    int cnt;
    cnt = (str[0][l] == 'x') + (str[1][l] == 'x') + (str[2][l] == 'x');
    f[l][cnt][1] = fac[cnt - 1];
    if (l != 1) {
        if (cnt == 2) 
            f[l][1][0] = 1;
        if (cnt == 3) 
            f[l][1][0] = f[l][2][0] = 2;
    }
    memset(p, 0, sizeof p);
    for (int j = 1; j <= cnt + 5; j++) p[j][1] = (p[j - 1][1] + f[l][j][1]) % P;
    for (int j = 1; j <= cnt + 5; j++) p[j][2] = (p[j - 1][2] + j * f[l][j][1]) % P;
    for (int j = cnt; j; j--) p[j][0] = (p[j + 1][0] + f[l][j][0]) % P;
    for (int i = l + 1; i <= r; i++) {
        int t = (str[0][i] == 'x') + (str[2][i] == 'x');
        cnt += t + 1;
        for (int j = 1; j <= cnt; j++) {
            // 1 -> 1
            int coe = fac[t] * C[j - 1][t] % P;
            Madd(f[i][j][1], p[cnt - t - 1][1] * coe % P);
            // 0 -> 1
            Madd(f[i][j][1], p[j - t][0] * coe % P);
            if (t) {
                // 1 -> 0
                Madd(f[i][j][0], p[j - 1][1] * C[cnt - j][t] % P * fac[t] % P);
                if (t != 1) {
                    coe = (cnt - j) * fac[t] % P;
                    Madd(f[i][j][0], (p[j - 1][1] * (j - 1) - p[j - 1][2] + P) % P * coe % P);
                    if (j >= 2) 
                        Madd(f[i][j][0], p[j - 2][2] * coe % P);
                }
            }
        }
        for (int j = 1; j <= cnt + 5; j++) p[j][1] = (p[j - 1][1] + f[i][j][1]) % P;
        for (int j = 1; j <= cnt + 5; j++) p[j][2] = (p[j - 1][2] + j * f[i][j][1]) % P;
        for (int j = cnt; j; j--) p[j][0] = (p[j + 1][0] + f[i][j][0]) % P;
    }
    ::cnt += cnt;
    int ret = 0;
    for (int i = 1; i <= cnt; i++) Madd(ret, (f[r][i][1] + (r != n) * f[r][i][0]) % P);
    return ret % P;
}
signed main() {
    cin >> n;
    for (int i = C[0][0] = fac[0] = 1; i <= n * 3; i++) {
        fac[i] = fac[i - 1] * i % P;
        for (int j = C[i][0] = 1; j <= i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
    }
    cin >> str[0] >> str[1] >> str[2];
    str[0] = ' ' + str[0], str[1] = ' ' + str[1], str[2] = ' ' + str[2];
    if (str[0][1] == 'x' || str[0][n] == 'x' || str[2][1] == 'x' || str[2][n] == 'x') 
        return cout << "0\n", 0;
    int c = 0, mx = 0;
    for (int i = 1; i <= n; i++) (str[0][i] == 'x') ? (mx = max(mx, ++c)) : (c = 0);
    if (mx > 1) 
        return cout << "0\n", 0;
    for (int i = 1; i <= n; i++) (str[2][i] == 'x') ? (mx = max(mx, ++c)) : (c = 0);
    if (mx > 1) 
        return cout << "0\n", 0;
    int _ = 0, ans = 1;
    for (int i = 1; i <= n; i++) {
        if (str[1][i] == 'o') 
            _ += (str[0][i] == 'x') + (str[2][i] == 'x');
        else {
            int t = cnt;
            int j = i;
            while (str[1][j] == 'x') ++j;
            ans = ans * work(i, j - 1) % P * C[cnt][t] % P;
            i = j - 1;
        }
    }
    cout << ans * C[cnt + _][_] % P * fac[_] % P << "\n";
    return 0;
}

T3

AT_joisc2019_e Two Dishes

首先每个限制可以表示为:完成 \(A_i\) 时,若 \(B\) 至多完成到第 \(j\) 个步骤,则获得 \(p_i\) 的收益,另一侧同理。于是显然有一个 \(n^2\) 的 dp,把它画到网格图上,则每个收益体现为一条前缀横线或竖线,经过一条横 / 竖线可以获得收益。

套路是对其中一维扫描线,但是不好处理两种限制。于是接下来我们考虑把两种限制统一。把横线变成竖线,容易变成后缀竖线。于是我们把原来的前缀竖线也变成后缀竖线。具体地,我们先把所有 \(p_i\) 算进答案,然后视为若完成 \(A_i\) 时已经完成了 \(B_j\),则获得 \(-p_i\) 的收益。横线也同理。然后我们的扫描线就只需要维护后缀加,加完之后每个数和前面 checkmax。可以想到维护差分数组,则每次相当于单点增加,然后一直向后合并直到当前值非负。可以使用 map 或 set 直接维护所有非 \(0\) 位置,每次暴力合并的复杂度就是对的。\(\mathcal{O}(n \log n)\)

代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <set>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, m, ans;
vector<pair<int, int> > vec[1000005];
set<pair<int, int> > st;
int a[1000005], b[1000005], s[1000005], t[1000005], p[1000005], q[1000005];
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i] >> s[i] >> p[i], a[i] += a[i - 1];
    for (int i = 1; i <= m; i++) cin >> b[i] >> t[i] >> q[i], b[i] += b[i - 1];
    for (int i = 1; i <= n; i++) {
        if (s[i] >= a[i]) {
            ans += p[i];
            int x = upper_bound(b + 1, b + m + 1, s[i] - a[i]) - b;
            if (x <= m) 
                vec[i].emplace_back(make_pair(-p[i], x));
        }
    }
    for (int i = 1; i <= m; i++) {
        if (t[i] >= b[i]) {
            int x = upper_bound(a + 1, a + n + 1, t[i] - b[i]) - a;
            if (x <= n) 
                vec[x].emplace_back(make_pair(q[i], i));
            else 
                ans += q[i];
        }
    }
    st.insert(make_pair(m + 1, inf));
    for (int x = 1; x <= n; x++) {
        sort(vec[x].begin(), vec[x].end(), greater<pair<int, int> >());
        for (auto v : vec[x]) {
            if (v.first >= 0) {
                pair<int, int> p = *st.lower_bound(make_pair(v.second, -inf));
                if (p.first == v.second) {
                    st.erase(p);
                    p.second += v.first;
                    st.insert(p);
                } else 
                    st.insert(make_pair(v.second, v.first));
            } else {
                while (v.first < 0) {
                    pair<int, int> p = *st.lower_bound(make_pair(v.second, -inf));
                    v.first += p.second;
                    v.second = p.first;
                    st.erase(p);
                }
                st.insert(make_pair(v.second, v.first));
            }
        }
    }
    for (auto v : st) {
        if (v.first <= m) 
            ans += v.second;
    }
    cout << ans << "\n";
    return 0;
}

T4

AT_joisc2018_d Asceticism

人话:求所有长为 \(n\) 的排列中,有多少恰有 \(K\)\(i \in [1, n)\) 满足 \([p_i > p_{i + 1}]\)

首先二项式反演变成至少,然后我们开始钦定。称满足条件的 \(i\) 是一个下降,则钦定 \(K\) 个位置是下降后,原序列会出现 \(n - K\) 个递减段(长度为 \(1\) 也算一段)。然后我们考虑往这些段里填数,相当于把 \(n\) 个互相区分的球放入 \(n - K\) 个互相区分的盒子,并保证每个盒子非空的方案数。对这个东西容斥,钦定只有一些盒子里能放球,并允许空盒子(实际上是二项式反演,恰好转至多)。允许空盒子之后的方案数是好算的,其实就是 \(m^n\),其中 \(m\) 是钦定的盒子数。于是可以列出式子:

\(\begin {equation}\begin {split} f(k) &= \sum\limits_{j = k}^{n - 1}(-1)^{j - k}\binom{j}{k}\sum\limits_{i = 0}^{n - j}(-1)^{n - j - i}\binom{n - j}{i}i^n \\ &= \sum\limits_{i = 0}^{n - k}\sum\limits_{j = k}^{n - i}(-1)^{n - k - i}i^n\binom{j}{k}\binom{n - j}{i} \\ &= \sum\limits_{i = 0}^{n - k}(-1)^{n - k - i}i^n\sum\limits_{j = k}^{n - i}\binom{j}{k}\binom{n - j}{i} \\ &= \sum\limits_{i = 0}^{n - k}(-1)^{n - k - i}i^n\binom{n + 1}{k + i + 1} \end {split} \end {equation}\)

根据这个式子即可快速计算一个 \(k\)\(f(K)\)。也可以直接多项式算出所有 \(f(K)\)

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 1000000007;
int qpow(int x, int y) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int fac[2000005], ifac[2000005], inv[2000005];
void Cpre(int n) {
    fac[0] = fac[1] = ifac[0] = ifac[1] = inv[0] = inv[1] = 1;
    for (int i = 2; i <= n; i++) {
        fac[i] = fac[i - 1] * i % P;
        inv[i] = (P - P / i) * inv[P % i] % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
}
inline int C(int n, int m) { return fac[n] * ifac[m] % P * ifac[n - m] % P; }
int n, K;
signed main() {
    Cpre(200000);
    cin >> n >> K;
    --K;
    int ans = 0;
    for (int i = 0; i <= n - K; i++) ans += qpow(P - 1, n - K - i) * qpow(i, n) % P * C(n + 1, K + i + 1) % P;
    cout << ans % P << "\n";
    return 0;
}

T6

uoj22 外星人

首先注意到模了小的之后,再模大的就不再有意义。于是可以考虑先把所有模数降序排序,然后倒着考虑到底模了哪些数。容易发现最后一次取模一定是模最小值。我们考虑直接倒着 dp,\(f_{i, j}\) 表示把 \(j\) 传进 \(i \sim n\),最大能得出什么数,以及得出这个最大值的方案数。转移是 \(\mathcal{O}(1)\) 的。数据范围比较小,这个 dp 直接就能过了。时间复杂度 \(\mathcal{O}(nV)\)

代码
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int P = 998244353;
int n, X;
int a[1005];
int f[1005][5005], g[1005][5005];
signed main() {
    cin >> n >> X;
    for (int i = 1; i <= n ;i++) cin >> a[i];
    sort(a + 1, a + n + 1, greater<int>());
    for (int i = 0; i <= X; i++) f[n][i] = i % a[n], g[n][i] = 1;
    for (int i = n - 1; i; i--) {
        for (int j = 0; j <= X; j++) {
            f[i][j] = f[i + 1][j], g[i][j] = g[i + 1][j] * (n - i) % P;
            int tf = f[i + 1][j % a[i]], tg = g[i + 1][j % a[i]];
            if (tf > f[i][j]) 
                f[i][j] = tf, g[i][j] = tg;
            else if (tf == f[i][j]) 
                g[i][j] = (g[i][j] + tg) % P;
        }
    }
    cout << f[1][X] << "\n" << g[1][X] << "\n";
    return 0;
}

T7

uoj497 新年的复读机

首先需要注意到结论:一定存在一种最优解使得它是从一个数开始,不断合并左右得到的。证明可以看官解。有了这个结论之后,考虑每个数往两边合并的过程,一定是一直往一边合并直到 gcd 变化,然后再决策接下来往哪一边。又有一个数往左往右 gcd 都是最多变化 \(\log\) 次,于是枚举起点,我们就有了一个 \(\mathcal{O}(n \log^2V)\) 的 dp。不过很可惜这个做法被卡了。不过很可惜加个卡时就能过了。

代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <time.h>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
inline int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
inline void Cmin(int &x, int y) { x = min(x, y); }
int n;
int a[200005], ASDF[200005], *pre = &ASDF[1];
struct node {
    int l, r, v;
    node(int x = 0, int y = 0, int z = 0) { l = x, r = y, v = z; }
};
vector<node> vl[200005], vr[200005];
int f[300][300], g[300][300];
signed main() {
    int ttt = clock();
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i], pre[i] = pre[i - 1] + a[i];
    for (int i = 1; i <= n; i++) {
        vl[i] = vl[i - 1];
        vector<node> &vec = vl[i], tmp;
        while (vec.size()) {
            node t = vec.back();
            if (gcd(t.v, a[i]) != t.v) {
                tmp.emplace_back(t.l, t.r, gcd(t.v, a[i]));
                vec.pop_back();
            } else 
                break;
        }
        while (tmp.size()) {
            if (!vec.size() || vec.back().v != tmp.back().v) 
                vec.emplace_back(tmp.back());
            else 
                vec.back().r = tmp.back().r;
            tmp.pop_back();
        }
        if (vec.empty() || a[i] != vec.back().v) 
            vec.emplace_back((node) { i, i, a[i] });
        else 
            vec.back().r = i;
    }
    for (int i = n; i; i--) {
        vr[i] = vr[i + 1];
        vector<node> &vec = vr[i], tmp;
        while (vec.size()) {
            node t = vec.back();
            if (gcd(t.v, a[i]) != t.v) {
                tmp.emplace_back(t.l, t.r, gcd(t.v, a[i]));
                vec.pop_back();
            } else 
                break;
        }
        while (tmp.size()) {
            if (!vec.size() || vec.back().v != tmp.back().v) 
                vec.emplace_back(tmp.back());
            else 
                vec.back().l = tmp.back().l;
            tmp.pop_back();
        }
        if (vec.empty() || a[i] != vec.back().v) 
            vec.emplace_back((node) { i, i, a[i] });
        else 
            vec.back().l = i;
    }
    pre[n + 1] = pre[n];
    int ans = inf;
    for (int i = 1; i <= n; i++) {
        reverse(vl[i].begin(), vl[i].end());
        reverse(vr[i].begin(), vr[i].end());
        int a = vl[i].size(), b = vr[i].size();
        for (int x = 0; x < a; x++) {
            g[x][0] = vl[i][x].v;
            for (int y = 1; y < b; y++) g[x][y] = gcd(g[x][y - 1], vr[i][y].v);
        }
        for (int x = 0; x <= a; x++) {
            for (int y = 0; y <= b; y++) 
                f[x][y] = inf;
        }
        f[0][0] = 0;
        for (int x = 0; x <= a; x++) {
            for (int y = 0; y <= b; y++) {
                if (x != a) 
                    Cmin(f[x + 1][y], f[x][y] + g[x - (x == a)][y - (y == b)] * (vl[i][x].r - vl[i][x].l + (x != a - 1)) + pre[vl[i][x].r - 1] - pre[vl[i][x].l - 2]);
                if (y != b) 
                    Cmin(f[x][y + 1], f[x][y] + g[x - (x == a)][y - (y == b)] * (vr[i][y].r - vr[i][y].l + (y != b - 1)) + pre[vr[i][y].r + 1] - pre[vr[i][y].l]);
            }
        }
        Cmin(ans, f[a][b]);
        if ((1.0 * clock() - ttt) / CLOCKS_PER_SEC >= 1.8) 
            break;
    }
    cout << ans << "\n";
    return 0;
}

T9

AT_agc036_d Negative Cycle

第一步要想到转化为差分约束模型。不存在负环等价于差分约束存在解,也就是存在一组 \(p_i\) 满足 \(\forall e(u \rightarrow v, w)\),都有 \(p_v \le p_u + w\)。由于原图边,也就是一条 \(0\) 权链的存在,我们有 \(p_{i + 1} \le p_i\)。然后考虑若 \(p\) 已经确定,则我们可以怎么连边。设第 \(i\) 个点的值是 \(p_i\),则前面所有 \(p_j = p_i\) 的点不能往 \(i\) 连,但可以从 \(i\) 连过去。前面所有 \(p_j = p_i + 1\) 的点既可以从 \(i\) 连过去,也可以连到 \(i\)。再前面所有 \(p_j > p_i + 1\) 的点,只能往 \(i\) 连而不能连过去。这样,我们发现特殊的就是 \(p_j = p_i\) 的这一段点。可以直接考虑 dp,\(f_{i, j}\) 表示当前到 \(i\)\([j, i]\) 这一段的 \(p\) 都和 \(p_i\) 相等,此时最大能保留多少权值的边。转移考虑下一段的 \(p\) 的终点放在哪个点,枚举的同时维护转移系数,总复杂度即为 \(\mathcal{O}(n^3)\)

代码
#include <iostream>
#include <string.h>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
inline void Cmax(int &x, int y) { x = max(x, y); }
int n, S;
int A[505][505], Sh[505][505], Sv[505][505];
int dp[505][505];
signed main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i != j) 
                cin >> A[i][j];
            S += A[i][j];
            Sh[i][j] = Sh[i][j - 1] + A[i][j];
            Sv[i][j] = Sv[i - 1][j] + A[i][j];
        }
    }
    memset(dp, -63, sizeof dp);
    dp[0][0] = 0;
    for (int i = 0; i <= n; i++) {
        for (int j = i; j <= n; j++) {
            int v = 0;
            for (int k = j + 1; k <= n; k++) {
                v += Sh[k][j] - (i ? Sh[k][i - 1] : 0);
                v += Sv[j][k] - (i ? Sv[i - 1][k] : 0);
                v += Sh[k][k] - Sh[k][j];
                v += (i ? Sv[i - 1][k] : 0);
                Cmax(dp[j + 1][k], dp[i][j] + v);
            }
        }
    }
    int ans = -inf;
    for (int i = 1; i <= n; i++) Cmax(ans, dp[i][n]);
    cout << S - ans << "\n";
    return 0;
}

T10

AT_agc040_e Prefix Suffix Addition

考虑如果只用第一种操作完成,则需要的操作次数是极长递增段的个数。只用第二种则是极长递减段的段数。然后需要想到将每个数 \(a_i\) 拆成两部分 \(x_i + y_i = a_i\),其中 \(x_i\) 是所有第一种操作得到的序列,\(y_i\) 是第二种得到的。然后我们考虑直接 dp,\(f_{i, v}\) 表示考虑完前 \(i\) 个,第 \(i\) 个数的 \(x_i = v\) 的最小代价。转移枚举下一个放啥,容易算出转移系数。但是暴力做过不去,但是会发现这个 dp 随着第二维增大其值减小,而且最多只有两个分界点使得左右的值不一样。这可以通过观察转移并使用归纳法得出。因此只需要维护第一项 dp 值以及两个分界点的位置即可。

代码
#include <iostream>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, a[200005], dp[200005];
signed main() {
    cin >> n;
    int v[2] = { 0, 0 };
    for (int i = 1; i <= n + 1; i++) {
        if (i <= n) 
            cin >> a[i];
        int tmp[3] = { inf, inf, 0 }, d[2] = { 0, a[i] - a[i - 1] };
        if (d[0] < d[1]) 
            swap(d[0], d[1]);
        for (int x = 0; x < 2; x++) {
            for (int y = 0; y < 2; y++) 
                tmp[x + y] = min(tmp[x + y], max(0ll, v[x] + d[y]));
        }
        int b = (tmp[0] > a[i]);
        dp[i] = dp[i - 1] + b;
        v[0] = tmp[b], v[1] = tmp[b + 1];
    }
    cout << dp[n + 1] << "\n";
    return 0;
}

T11

AT_agc020_f Arcs on a Circle

先钦定最长的弧的起点作为环的起点。然后实数不好处理,考虑拆成整数部分加小数部分。对于小数部分,可以认为两两不会相等,我们只关心它们之间的大小关系,而不关心具体值。于是我们可以暴力枚举每个数小数部分的大小关系,然后朴素地枚举每个弧的整数部分放在环的哪个整点上,然后判断合法。这样做复杂度爆炸,但是我们可以注意到整个环上重要的只有 \(nC\) 个点,也就是每个弧的小数部分对应每一个整点。我们可以对着这 \(nC\) 个点 dp,dp 时记录当前已经用了哪些弧,再记录最远覆盖到了什么位置。也就是 \(f_{i, j, k}\) 表示考虑了前 \(i\) 个点,已经用了 \(S\) 中的弧,最远覆盖到 \(k\) 这个点。转移就考虑第 \(i\) 个点放弧的话只能放 \(i \bmod n\) 这个弧,如果还没有被放过的话就看一下要不要放。总之转移是 \(\mathcal{O}(1)\) 的。总复杂度懒得算,但是 \(\mathcal{O}(能过)\)

代码
#include <iostream>
#include <algorithm>
#include <string.h>
#include <iomanip>
using namespace std;
int n, C;
int a[200005], p[200005];
int dp[305][65][305];
int main() {
    cin >> n >> C;
    for (int i = 0; i < n; i++) cin >> a[i], p[i] = i;
    sort(a, a + n, greater<int>());
    double ans = 0;
    do {
        memset(dp, 0, sizeof dp);
        dp[0][1][a[0] * n] = 1;
        for (int i = 1; i < n * C; i++) {
            for (int j = 1; j < (1 << n); j++) {
                for (int k = i; k <= n * C; k++) {
                    if (!dp[i - 1][j][k]) 
                        continue;
                    int x = p[i % n];
                    if (!(j & (1 << x))) 
                        dp[i][j | (1 << x)][min(n * C, max(i + n * a[x], k))] += dp[i - 1][j][k];
                    dp[i][j][k] += dp[i - 1][j][k];
                }
            }
        }
        ans += dp[n * C - 1][(1 << n) - 1][n * C];
    } while (next_permutation(p + 1, p + n));
    for (int i = 1; i < n; i++) ans /= i, ans /= C;
    cout << fixed << setprecision(12) << ans << "\n";
    return 0;
}

T12

AT_agc038_e Gachapon

上来先 min-max 容斥一下,变成集合中第一次有数爆掉的期望时间。接下来我们考虑集合 \(T\),不考虑其他元素。设 \(A(S)\) 表示集合 \(S\)\(a\) 的和,\(B(S)\) 表示集合 \(S\)\(b\) 的和。去除其他元素的影响后,只需要最后将步数乘上 \(\frac{A(U)}{A(T)}\) 即可得到最终结果。接下来考虑集合 \(T\) 中。我们枚举时间 \(i\),并算出时间 \(i\) 时游戏仍在进行的概率。我们考虑枚举每个元素各选了几个,则概率可以表示为 \(p(i) = \frac{\sum\limits_{x} \prod\frac{a_j^{x_j}}{x_j!}}{A(T)^i}\),其中 \(x\) 是其中所有数和为 \(i\) 的整数数组。于是最终答案即为 \(\sum\limits_{T \subseteq U}(-1)^{|T| + 1}\frac{A(U)}{A(T)}\sum\limits_{i = 1}^{B(T)}\frac{i!}{A(T)^i}\sum\limits_x\prod\frac{a_j^{x_j}}{x_j!}\)。我们考虑直接 dp 维护这个东西,\(f_{i, j, k}\) 表示考虑完了前 \(i\) 个数,当前选的 \(A(T) = j, B(T) = k\) 的所有情况的后面那个乘积的带容斥系数的和。转移考虑当前这个东西选不选、选几个。这个 dp 看起来是 \(\mathcal{O}(n^4)\) 的,但是由于每个物品选几个这个的枚举总量是 \(\mathcal{O}(n)\) 的,所以这个 dp 的复杂度其实是 \(\mathcal{O}(n^3)\)。最后统计答案部分是容易的。注意空集的容斥系数是 \(-1\),初始化时需要特别注意。

代码
#include <iostream>
#include <string.h>
#define int long long
using namespace std;
const int P = 998244353;
int qpow(int x, int y = P - 2) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int fac[2005], ifac[2005], inv[2005];
void Cpre(int n) {
    fac[0] = fac[1] = ifac[0] = ifac[1] = inv[0] = inv[1] = 1;
    for (int i = 2; i <= n; i++) {
        fac[i] = fac[i - 1] * i % P;
        inv[i] = (P - P / i) * inv[P % i] % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
}
int n, sa, sb;
int A[405], B[405];
int pw[405][405];
int f[2][405][405];
signed main() {
    Cpre(1000);
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> A[i] >> B[i]; sa += A[i], sb += B[i];
        pw[i][0] = 1;
        for (int j = 1; j <= 400; j++) pw[i][j] = pw[i][j - 1] * A[i] % P;
    }
    f[0][0][0] = -1;
    for (int i = 1; i <= n; i++) {
        int cur = (i & 1), lst = (cur ^ 1);
        memset(f[cur], 0, sizeof f[cur]);
        for (int j = 0; j <= sa; j++) {
            for (int k = 0; k <= sb; k++) {
                f[cur][j][k] = f[lst][j][k];
                if (A[i] <= j) {
                    for (int l = 0; l <= min(k, B[i] - 1); l++) 
                        f[cur][j][k] -= f[lst][j - A[i]][k - l] * pw[i][l] % P * ifac[l] % P;
                }
                f[cur][j][k] = (f[cur][j][k] % P + P) % P;
            }
        }
    }
    int ans = 0;
    for (int i = 0; i <= sa; i++) {
        for (int j = 0; j <= sb; j++) 
            ans += f[n & 1][i][j] * sa % P * fac[j] % P * qpow(i, (j + 1) * (P - 2)) % P;
        ans %= P;
    }
    cout << ans << "\n";
    return 0;
}

对判定过程计数。

dp 中记录排名以保证相对顺序。

转化统一限制。

范德蒙德卷积 \(\sum\limits_{i = 0}^{n + 1}\binom{i}{x}\binom{n - i}{y} = \binom{n + 1}{x + y + 1}\),组合意义是 \(n + 1\) 里选 \(x + y + 1\) 个,枚举第 \(x + 1\) 个选在哪,前面选 \(x\) 个,后面选 \(y\) 个。

负环考虑差分约束。

实数拆成整数与小数,只关心需要关心的。

min-max 容斥。

posted @ 2025-02-22 00:18  forgotmyhandle  阅读(65)  评论(0)    收藏  举报