dp递推 口胡记录

[SHOI2013] 超级跳马

\(tag\):矩阵乘法,前缀和

暴力\(dp\)很显然,设\(f_{i,j}\)为从\((1,1)\)跳到\((i,j)\)的方案数,那么有$f_{i,j}= \sum \limits _{j-(2k+1)>0}f _{i/i+1/i-1,j-(2k+1)} $

发现这个东西其实是一直由前面奇偶性相同的一段转移过来的,因此考虑前缀和优化

\(s_{i,j}=\sum \limits _{j-2k>0} f_{i,j-2k}\),那么由定义有\(s_{i,j}=s_{i,j-2}+f_{i,j}\),然后我们再考虑\(f\)怎么由\(s\)转移而来,不难发现\(f_{i,j}=s_{i-1,j-1}+s_{i,j-1}+s_{i+1,j-1}\),联立两式有\(s_{i,j}=s_{i,j-2}+s_{i,j-1}+s_{i-1,j}+s_{i+1,j-1}\)

然后这个前缀和显然可以矩乘转移。

[JLOI2015] 有意义的字符串

不看题解连矩乘都想不到,/kk

发现\(\frac{b+ \sqrt{d}}{2}\)的形式和一元二次方程求根公式很像,进过构造可以发现它是方程\(x^2-bx+\frac{b^2-d}{4}=0\)的一个解。

移项后两边同时乘\(x^{n-2}\)\(x^n=bx^{n-1}-\frac{b^2-d}{4}x^{n-2}\)

发现这个已经形成了矩乘形式的递推关系,但是初始\(\frac{b+ \sqrt{d}}{2}\)是小数,不方便进行取模。

所以考虑构造\(f(x)=(\frac{b+ \sqrt{d}}{2})^x+(\frac{b- \sqrt{d}}{2})^x\),将两个根\(x1,x2\)带入后两式相加发现有\(f(i)=bf(i-1)-\frac{b^2-d}{4}f(i-2)\),并且有\(f(0),f(1),b,\frac{b^2-d}{4}\)都为整数。

因此可以直接矩乘计算出\(f(n)\),再考虑减去\((\frac{b- \sqrt{d}}{2})^n\)的贡献,发现这东西 \(\in \left\{-1,1 \right\}\)分类讨论\(n\)的奇偶性判断答案是否减一即可。

注意用__int128或者龟速乘。

[ABC219H] Candles

首先有经典的关灯模型,显然每次关灯都是在已选的区间向左或向右拓展一格。那么设\(dp_{l,r,t,0/1}\)为已选区间为\([l,r]\) ,时间为 \(t\) ,在 \([l,r]\) 的左/右端点的蜡烛长度最大值。

但是时间这一维很大,考虑优化,一种比较套路的想法是先将所有蜡烛熄灭,然后时间倒流点燃,对于每个时刻,答案会损失点燃的长度非负蜡烛数。

因此每个蜡烛的贡献为\(\max(0, a_i - t)\),这个式子不好维护。因为存在某些蜡烛最终贡献为0,考虑只记录有效的点。具体来说,设\(dp_{l,r,k,0/1}\),其中\(l,r,0/1\)与前文的定义相同,\(k\)表示\([l,r]\)区间外有\(k\)个点有效,会对答案造成贡献。然后就很好做了。

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

点击查看代码
#include <bits/stdc++.h>

#define ll long long
const int MAXN = 305;
ll dp[MAXN][MAXN][MAXN][2];

void solve() {
    int n;
    std::cin >> n;
    std::vector <std::pair<int, int> > a(n + 1);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i].first >> a[i].second;
    }
    a[n] = std::make_pair(0, 0);

    std::sort(a.begin(), a.end());
    int beg = -1;
    for (int i = 0; i < a.size(); i++) {
        if (a[i].second == 0) {
            beg = i;
            break;
        }
    } 
    
    const ll INF = 1e15;
    for (int i = 0 ; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            for (int k = 0; k <= n; k++) {
                dp[i][j][k][0] = dp[i][j][k][1] = -INF;
            }
        }
    }

    for (int i = 0; i <= n; i++) {
        dp[beg][beg][i][0] = dp[beg][beg][i][1] = 0;
    }
    for (int len = 2; len <= n + 1; len++) {
        for (int l = 0; l <= n; l++) {
            int r = l + len - 1;
            if (r > n) break;
            for (int k = 0; k <= n; k++) {
                ll x1 = -INF, x2 = -INF;
                x1 = std::max(dp[l + 1][r][k][0] - 1ll * (a[l + 1].first - a[l].first) * k, dp[l + 1][r][k][1] - 1ll * (a[r].first - a[l].first) * k);
                if (k != n) x2 = std::max(dp[l + 1][r][k + 1][0] - 1ll * (a[l + 1].first - a[l].first) * (k + 1) + a[l].second, dp[l + 1][r][k + 1][1] - 1ll * (a[r].first - a[l].first) * (k + 1) + a[l].second);
                dp[l][r][k][0] = std::max(x1, x2); 

                x1 = std::max(dp[l][r - 1][k][0] - 1ll * (a[r].first - a[l].first) * k, dp[l][r - 1][k][1] - 1ll * (a[r].first - a[r - 1].first) * k);
                if (k != n) x2 = std::max(dp[l][r - 1][k + 1][0] - 1ll * (a[r].first - a[l].first) * (k + 1) + a[r].second, dp[l][r - 1][k + 1][1] - 1ll * (a[r].first - a[r - 1].first) * (k + 1) + a[r].second);
                dp[l][r][k][1] = std::max(x1, x2);                 
            }
        }
    }

    ll ans = -INF;
    for (int l = 0; l <= n; l++) {
        for (int r = l; r <= n; r++) {
            for (int k = 0; k <= n; k++) {
                ans = std::max(ans, std::max(dp[l][r][k][0], dp[l][r][k][1]));
            }
        }
    }

    std::cout << ans << "\n";
}

int main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);
    std::cout.tie(0);

    int t = 1;
    while (t--) {
        solve();
    }

    return 0;
}

[AGC012E] Camel and Oases

首先由于每次跳跃时\(v\)的值都会除以2,所以只有\(O(\log n)\)\(v\)值,不难想到固定一个\(v\)的值,可以算出每个点能向左/右拓展到的最远点,分别记为\(l_i, r_i\)

然后发现如果我们将每个固定的\(v\)形成的若干个区间的集合看成一层,那么原问题等价于对于每个\(i\)在除了\(v'=v\)这一层中之后的每一层都选恰好一个区间,能否使得它们的并集为\([1,n]\),简称能为可达

那么显然如果\(i\)可达\([1,n]\),就有从\(1\)开始的前缀和从\(n\)开始的后缀能够覆盖\(i\)所在区间。考虑设\(dpl_{mask}\)为在状态为\(mask\)的层中选择了点能覆盖前缀的最大值,\(dpr_{mask}\)同理,很好转移。

对于每个\(i\), 若\(dpl_{mask}\) 与 $ dpr_{mask2} $ 的交覆盖了\(i\)所在的区间 $ (mask1 | mask2 = all) $,则这个区间是可行的。

复杂度\(O(n \log v)\)

扩张

一类形如 \(dp_{mask} = (|mask| - |sub|) \times w(mask)\)的转移方程的优化,\(O(3 ^ n) -> O(2 ^ n \times n ^2)\)

image

[ARC101E] Ribbons on Tree

三个月前做过,现在不会了,不像某位神仙两年前做过现在一眼秒了😅

比较navie的想法是设\(dp{u,x}\)\(u\)的子树中有\(x\)个点未被覆盖,需要子树外一个点向内某个点相连的方案数,直接类似树背包转移有 \(dp_{u,x + y - 2k} = \sum \limits_{v \in son(u), x +y > 0,k \leq min(x, y)} dp_{u, x} \times dp_{v,y} \times \binom{x}{k} \times \binom{y}{k} \times k!\)

系数过于复杂,不便于优化。看到每条边至少被覆盖一次考虑容斥。

考虑边集\(S\)中的边一次都没被覆盖过,这等价于删去这些边。此时整张图被分成了若干个联通块,显然每个联通块内部匹配的方案数是 \(g(n) = \prod \limits _{2k \leq n, k \in N^*}{2k - 1}\),注意\(n\)为奇数时\(g(n) = 0\)

数据范围无法枚举子集,考虑\(dp\),重定义\(dp_{u,x}\)为以\(u\)为根的子树联通块大小为\(x\)的方案数(考虑容斥系数)。那么有这条边断掉 \(dp_{u,x} \times dp_{v,y} \times{g(y)} \times -1 \rightarrow dp_{u,x}\) (断边集合大小加一),保留这条边 \(dp_{u,x} \times dp_{v,y} \rightarrow dp_{u, x +y}\)

最后答案即为 \(\sum \limits_{i = 1} ^n dp_{1, i} \times g(i)\)。复杂度为树形背包的 \(O(n ^2)\)

点击查看代码
#include <bits/stdc++.h>

void solve() {
    int n;
    std::cin >> n;
    std::vector <std::vector<int> > adj(n + 1);
    for (int i = 1; i < n; i++) {
        int u, v;
        std::cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    const int MOD = 1e9 + 7;
    std::vector <int> g(n + 1);
    g[0] = 1;
    for (int i = 2; i <= n; i += 2) g[i] = 1ll * g[i - 2] * (i - 1) % MOD; 

    std::vector <std::vector<int> > dp(n + 1);
    std::vector <int> siz(n + 1);
    for (int i = 0; i <= n; i++) dp[i].resize(n + 1);
    auto dfs = [&](auto self, int u, int fa) -> void {
        siz[u] = 1;
        dp[u][1] = 1;
        for (const auto &v : adj[u]) {
            if (v == fa) continue;
            self(self, v, u);
            std::vector <int> tmp(siz[u] + siz[v] + 1);
            for (int j = 1; j <= siz[u]; j++) {
                for (int k = 1; k <= siz[v]; k++) {
                    tmp[j] += -1ll * dp[u][j] * dp[v][k] % MOD * g[k] % MOD;
                    tmp[j] %= MOD; tmp[j] += MOD; tmp[j] %= MOD;
                    tmp[j + k] += 1ll * dp[u][j] * dp[v][k] % MOD;
                    tmp[j + k] %= MOD;
                }
            }
            siz[u] += siz[v];
            for (int i = 1; i <= siz[u]; i++) dp[u][i] = tmp[i];
        }
    };
    dfs(dfs, 1, 0);
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        ans += 1ll * dp[1][i] * g[i] % MOD;
        ans %= MOD;
    }

    std::cout << ans << "\n";
}

int main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);
    std::cout.tie(0);

    int t = 1;
    while (t--) {
        solve();
    }

    return 0;
}

「GLR-R3」惊蛰

tag:\(dp\)优化,线段树

暴力\(dp\)很显然,状态为设\(dp_{i,j}\)\(i\)\(b\),其中最后一个数为\(j\)的最小答案,可以暴力 \(O(nV^2)\)转移。

发现可以前缀最小值优化,并且第二维的取值只可能是\(a_i\),于是可以离散化优化到\(O(n ^ 2)\)

设离散化后数组为\(a'\),离散化数组为\(lsh\),考虑变换\(dp\)状态,重定义\(dp_{i,j}\)\(lsh_j \leq b_i\)时最小值(即原dp的后缀和),那么\(dp\)转移过程为image
image
发现这种形式可以用线段树维护。

具体来说,第一种转移直接区间加。第二种主要是区间加\(lsh_j\)打标记即可。主要难在保持\(dp_i\)的单调性。

但是又发现\(lsh\)\(dp_i\)两个数组都是单调的,加完后\(dp_{i,1} \rightarrow dp_{i,k}\)\(dp_{i,k+1} \rightarrow dp_{i,n}\)都是单调的。为了维护整个\(dp_i\)单调性,用第一部分的最小值,在线段树上二分出取\(min\)的影响区间,然后区间覆盖即可。

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

点击查看代码
#include <bits/stdc++.h>

const int MAXN = 1e6 + 5;

template <class T>
void read(T &x) {
    x = 0; char c = getchar(); bool f = 0;
    while (!isdigit(c)) f = c == '-', c = getchar();
    while (isdigit(c)) x = x * 10 + c - '0', c = getchar();
    x = f ? (-x) : x;
}

#define ll long long

int lsh[MAXN];
struct Sgt {
    struct Node {
        int l, r;
        ll mx;
        ll add, tag, addb;
    }t[MAXN * 8];
    void pushup(int p) {
        t[p].mx = std::max(t[p << 1].mx, t[p << 1 | 1].mx);
    }
    void pushadd(int p, ll tg) {
        t[p].mx += tg; t[p].add += tg;
    }
    void pushaddb(int p, ll tg) {
        t[p].mx += 1ll * lsh[t[p].l] * tg; t[p].addb += tg;
    }
    void pushtag(int p, ll tg) {
        t[p].mx = t[p].tag = tg;
        t[p].add = t[p].addb = 0;
    }
    void pushdown(int p) {
        if (t[p].tag != -1) {
            pushtag(p << 1, t[p].tag); pushtag(p << 1 | 1, t[p].tag);
            t[p].tag = -1;
        }
        if (t[p].add) {
            pushadd(p << 1, t[p].add); pushadd(p << 1 | 1, t[p].add);
            t[p].add = 0;
        }
        if (t[p].addb) {
            pushaddb(p << 1, t[p].addb); pushaddb(p << 1 | 1, t[p].addb);
            t[p].addb = 0;
        }
    }
    void build(int p, int l, int r) {
        t[p].l = l; t[p].r = r;
        t[p].tag = -1;
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
    }
    void add(int p, int l, int r, ll delta) {
        if (l <= t[p].l && t[p].r <= r) {
            pushadd(p, delta);
            return;
        }
        if (t[p].l != t[p].r) pushdown(p);
        int mid = (t[p].l + t[p].r) >> 1;
        if (l <= mid) add(p << 1, l, r, delta);
        if (r > mid) add(p << 1 | 1, l, r, delta);
        pushup(p);
    }
    void addb(int p, int l, int r, ll delta) {
        if (l <= t[p].l && t[p].r <= r) {
            pushaddb(p, delta);
            return;
        }
        if (t[p].l != t[p].r) pushdown(p);
        int mid = (t[p].l + t[p].r) >> 1;
        if (l <= mid) addb(p << 1, l, r, delta);
        if (r > mid) addb(p << 1 | 1, l, r, delta);
        pushup(p);        
    }
    void assign(int p, int l, int r, ll v) {
        if (l <= t[p].l && t[p].r <= r) {
            pushtag(p, v);
            return;
        }
        if (t[p].l != t[p].r) pushdown(p);
        int mid = (t[p].l + t[p].r) >> 1;
        if (l <= mid) assign(p << 1, l, r, v);
        if (r > mid) assign(p << 1 | 1, l, r, v);
        pushup(p); 
    }
    ll query(int p, int pos) {
        if (t[p].l == t[p].r) {
            return t[p].mx;
        }
        if (t[p].l != t[p].r) pushdown(p);
        int mid = (t[p].l + t[p].r) >> 1;
        if (pos <= mid) return query(p << 1, pos);
        return query(p << 1 | 1, pos);
    }
    int binary(int p, ll x) {
        if (t[p].l != t[p].r) pushdown(p);
        if (t[p].mx < x) return -1;
        else if (t[p].l == t[p].r) return t[p].l;
        int mid = (t[p].l + t[p].r) >> 1;
        int ret = binary(p << 1 | 1, x);
        if (ret != -1) return ret;
        return binary(p << 1, x);
    }
    //last position i where dp[i] >= x
};

void solve() {
    int n, c;
    std::cin >> n >> c;
    std::vector <int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        read(a[i]);
        lsh[i] = a[i];
    }
    std::sort(lsh + 1, lsh + 1 + n);
    int tot = std::unique(lsh + 1, lsh + 1 + n) - lsh - 1;
    for (int i = 1; i <= n; i++) {
        a[i] = std::lower_bound(lsh + 1, lsh + 1 + tot, a[i]) - lsh;
        a[i] = tot - a[i] + 1;
        // std::cerr << a[i] << " ";
    }
    // std::cerr << "\n";
    std::reverse(lsh + 1, lsh + 1 + tot);

    static Sgt t;
    t.build(1, 1, tot);
    for (int i = 1; i <= n; i++) {
        if (a[i] + 1 <= tot) t.add(1, a[i] + 1, tot, c);
        t.add(1, 1, a[i], -lsh[a[i]]);
        t.addb(1, 1, a[i], 1);
        ll val = t.query(1, a[i]);
        // std::cerr << "qwq" << "\n";
        int pos = t.binary(1, val);
        // int pos = tot;
        // std::cerr << "qaq" << "\n";
        if (pos != -1 && a[i] + 1 <= pos) t.assign(1, a[i] + 1, pos, val);
    }

    ll ans = 1e18;
    for (int i = 1; i <= tot; i++) ans = std::min(ans, t.query(1, i));
    std::cout << ans << "\n";
}

int main() {
    // freopen("wave.in", "r", stdin);
    // freopen("wave.out", "w", stdout);
    // std::ios::sync_with_stdio(0);
    // std::cin.tie(0);
    // std::cout.tie(0);

    int t = 1;
    while (t--) {
        solve();
    }

    return 0;
}
posted @ 2023-10-27 22:41  Katyusha_Lzh  阅读(24)  评论(0)    收藏  举报