动态规划优化 习题总结 1

动态规划优化 习题总结

《算法竞赛进阶指南》中的例题与练习题。

排名 - L5-2022-A2-动态规划优化 - 码创未来

*A. P1081 [NOIP2012 提高组] 开车旅行(倍增优化 DP)

洛谷 P1081 | AcWing 293 | 码创未来

题意

小 A 和小 B 开车去东西排列的连续 \(n\) 座城市旅行,城市自西向东编号依次为 \(1\)\(n\),已知各个城市的海拔高度互不相同,记城市 \(i\) 的海拔高度为 \(h_i\),则城市 \(i\) 和城市 \(j\) 之间的距离 \(d_{i,j}=|h_i-h_j|\)

旅行过程中,小 A 和小 B 轮流开车,第一天小 A 开车,之后每天轮换一次。他们计划选择一个城市 \(s\) 作为起点,一直向东行驶,并且最多行驶 \(x\) 公里就结束旅行。

小 A 和小 B 的驾驶风格不同,小 B 总是沿着前进方向选择一个最近的城市作为目的地,而小 A 总是沿着前进方向选择第二近的城市作为目的地(注意:本题中如果当前城市到两个城市的距离相同,则认为离海拔低的那个城市更近)。如果其中任何一人无法按照自己的原则选择目的城市,或者到达目的地会使行驶的总距离超出 \(x\) 公里,他们就会结束旅行。

在启程之前,小 A 想知道两个问题:

  1. 对于一个给定的 \(x=x_0\),从哪一个城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值最小(如果小 B 的行驶路程为 \(0\),此时的比值可视为无穷大,且两个无穷大视为相等)。如果从多个城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值都最小,则输出海拔最高的那个城市。

  2. 对任意给定的 \(x=x_i\) 和出发城市 \(s_i\),小 A 开车行驶的路程总数以及小 B 行驶的路程总数。

对于 \(100\%\) 的数据:\(1\le n,m \le 10^5\)\(-10^9 \le h_i≤10^9\)\(1 \le s_i \le n\)\(0 \le x_i \le 10^9\)

数据保证 \(h_i\) 互不相同。

思路

本题有三个关键信息:所在城市、行驶天数、A 和 B 行驶的总距离。

由出发城市和天数我们可以知道现在所在的城市,以及当前谁在开车,并且可以算出两人分别和总共行驶的距离。

天数是一个连续的阶段,我们可以用倍增来优化。还需要设一维状态表示谁先开车,因为中间过程中不一定是 A 先开车。

  • \(f(i,j,k)\) 表示从城市 \(j\) 出发,一共行驶了 \(2^i\) 天,出发时 \(k\) 先开车,最终能到达的城市,其中 \(k\in\{0,1\}\)\(k=0\) 表示 A 先开车,\(k=1\) 表示 B 先开车。

    对于初值 \(f(0,j,k)\),我们可以暴力处理出来。

    \(ga(j)\)\(gb(j)\) 分别表示 A 和 B 从 \(j\) 出发到达的下一个城市,\(i\in[j+1,n]\),那么 \(gb(j)\) 即是令 \(d_{i,j}\) 取得最小值的 \(i\)\(ga(j)\) 即是令 \(d_{i,j}\) 取得次小值的的 \(i\)。(注意 A 对应次小值,B 对应最小值。。。)

    我们从后往前遍历城市 \(j\),将 \(h_j\) 插入一个 std::multiset 即平衡树,这样已经在平衡树中的城市范围为 \([j,n]\),那么从当前城市 \(j\) 向前遍历两个、向后遍历两个城市并比较距离即可得出最近城市和第二近城市。具体实现可以参考代码。

    于是 \(f(0,j,0)=ga(j),f(0,j,1)=gb(j)\)。根据倍增的思想,并且特殊考虑 \(i=1\) 的情况(一共 \(2\) 天,第一天 \(k\) 开车,第二天 \(k\operatorname{xor}1\) 开车),得出状态转移方程。

    \[f(i,j,k)= \begin{cases} f(i-1,f(i-1,j,k),k\operatorname{xor}1)&k=1\\ f(i-1,f(i-1,j,k),k)&k\ne1 \end{cases}\qquad i\ge1,1\le j\le n,k\in\{0,1\}. \]

  • 接下来处理 A 和 B 行驶的距离。我们设 \(da(i,j,k)\)\(db(i,j,k)\) 分别表示从城市 \(j\) 出发,一共行驶了 \(2^i\) 天,出发时 \(k\) 先开车,A 和 B 行驶的总距离

    初值 \(da(0,j,0)=d_{j,ga(j)},da(0,j,1)=0\)\(db(0,j,0)=0,db(0,j,1)=d_{j,gb(j)}\)

    同样根据倍增思想,利用之前处理出的 \(f\) 数组,写出状态转移方程。

    \[\begin{gathered} da(i,j,k)= \begin{cases} da(i-1,j,k)+da(i-1,f(i-1,j,k),k\operatorname{xor}1)&k=1\\ da(i-1,j,k)+da(i-1,f(i-1,j,k),k)&k\ne1 \end{cases} \\ db(i,j,k)= \begin{cases} db(i-1,j,k)+db(i-1,f(i-1,j,k),k\operatorname{xor}1)&k=1\\ db(i-1,j,k)+db(i-1,f(i-1,j,k),k)&k\ne1 \end{cases} \\ \qquad i\ge1,1\le j\le n,k\in\{0,1\}. \end{gathered} \]

  • 预处理出天数的二进制每一位的状态之后,我们来考虑一般性的问题。

    \(calc(s,x)\) 表示从城市 \(s\) 出发,最多行驶 \(x\) 公里,A 和 B 行驶的路程。显然这个函数是一个二元组。

    根据二进制拆分的思想,我们递减枚举 \(2\)\(i\) 次幂,累加到行驶的天数里。

    具体来说,我们设所在城市为 \(p\),A 和 B 行驶的路程分别为 \(la\)\(lb\)。开始时 \(p=s,la=lb=0\)。然后倒序循环枚举 \(2\) 的幂次 \(i\)\(\log_2(n)\ge i\ge0\)。如果 \(la+lb+da(i,p,0)+db(i,p,0)\le x\),说明可以继续行驶 \(2^i\) 天,于是我们更新数值,令 \(la\gets la+da(i,p,0),lb\gets lb+db(i,p,0),p\gets f(i,p,0)\)

    循环结束后,所得的 \(la,lb\) 即为答案。

  • 有了 \(calc(s,x)\) 函数,我们就可以解决题目中提出的问题了。

    1. 枚举 \(s\),打擂台求出 \(\arg\min\{calc(s,x_0)\}\)
    2. 多次询问 \(calc(s_i,x_i)\)

代码

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <set>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
#define int long long
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
int n, m, l, ga, gb, f[20][N][2], da[20][N][2], db[20][N][2];

struct City {
    int id, h;
    City() {}
    City(int _id, int _h): id(_id), h(_h) {}
    bool operator<(City const &o) const {
        return h < o.h;
    }
} a[N], tmp[4];
multiset<City> myset;

inline bool cmp(City const &p, City const &q) {
    return p.h == q.h ? a[p.id].h < a[q.id].h : p.h < q.h;
}

pii calc(int s, int x) {
    int p = s;
    int la = 0, lb = 0;
    g(i, l, 0) {
        if (f[i][p][0] && la + lb + da[i][p][0] + db[i][p][0] <= x) {
            la += da[i][p][0];
            lb += db[i][p][0];
            p = f[i][p][0];
        }
    }
    return (pii){la, lb};
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n;
    l = log2(n);
    f(i, 1, n) cin >> a[i].h, a[i].id = i;
    myset.insert(City(0, INF)), myset.insert(City(0, INF));
    myset.insert(City(n + 1, -INF)), myset.insert(City(n + 1, -INF));
    g(i, n, 1) {
        auto it = myset.insert(a[i]); //insert()函数返回值正是刚刚插入的值的迭代器(这样就不用find()函数了)
        auto it1 = ++it, it2 = ++it, it3 = --(--(--it)), it4 = --it; //移动std::multiset<>::iterator只能用++和--
        tmp[0] = City((*it1).id, (*it1).h - a[i].h), tmp[1] = City((*it2).id, (*it2).h - a[i].h); //*iterator表示引用迭代器所
        tmp[2] = City((*it3).id, a[i].h - (*it3).h), tmp[3] = City((*it4).id, a[i].h - (*it4).h); //指向的元素, 也可以用it->h
        sort(tmp, tmp + 4, cmp);
        gb = tmp[0].id, ga = tmp[1].id;
        f[0][i][0] = ga, f[0][i][1] = gb;
        da[0][i][0] = abs(a[i].h - a[ga].h), db[0][i][1] = abs(a[i].h - a[gb].h);
    }
    f(j, 1, n) f(k, 0, 1) { //i=1
        f[1][j][k] = f[0][f[0][j][k]][k^1];
        da[1][j][k] = da[0][j][k] + da[0][f[0][j][k]][k^1];
        db[1][j][k] = db[0][j][k] + db[0][f[0][j][k]][k^1];
    }
    f(i, 2, l) f(j, 1, n) f(k, 0, 1) { //i>1
        f[i][j][k] = f[i-1][f[i-1][j][k]][k];
        da[i][j][k] = da[i-1][j][k] + da[i-1][f[i-1][j][k]][k];
        db[i][j][k] = db[i-1][j][k] + db[i-1][f[i-1][j][k]][k];
    }
    int s = 0, x;
    cin >> x;
    double minn = INF;
    f(i, 1, n) {
        pii tmp = calc(i, x);
        double sum = tmp.second == 0 ? INF : ((double)tmp.first / (double)tmp.second);
        if (sum < minn) minn = sum, s = i;
        else if (sum == minn && a[i].h > a[s].h) s = i;
    }
    cout << s << '\n';
    cin >> m;
    f(i, 1, m) {
        cin >> s >> x;
        pii tmp = calc(s, x);
        cout << tmp.first << ' ' << tmp.second << '\n';
    }
    
    return 0;
}

*B. Count The Repetitions(倍增优化 DP)

AcWing 294 | 码创未来

题意

定义 \(conn(s,n)\)\(n\) 个字符串 \(s\) 首尾相接形成的字符串,例如 \(conn(\texttt{abc},2)=\texttt{abcabc}\)

称字符串 \(a\) 能由字符串 \(b\) 生成,当且仅当从字符串 \(b\) 中删除某些字符后可以得到字符串 \(a\)

例如 \(\texttt{abdbec}\) 可以生成 \(\texttt{abc}\),但是 \(\texttt{acbbe}\) 不能生成 \(\texttt{abc}\)

给定两个字符串 \(s_1\)\(s_2\),以及两个整数 \(n_1\)\(n_2\),求一个最大的整数 \(m\),满足 \(conn(conn(s_2,n_2),m)\) 能由 \(conn(s_1,n_1)\) 生成。

\(|s_1|,|s_2|\le100\)\(n_1,n_2\le10^6\)。其中 \(|str|\) 表示字符串 \(str\) 的长度。

注意先输入 \(s_2,n_2\) 再输入 \(s_1,n_1\)。。。

思路

首先发现 \(conn(conn(s_2,n_2),m)=conn(s_2,n_2\times m)\)

所以我们求的其实是最大的 \(m'=n_2\times m\),满足 \(conn(s_2,m')\)\(conn(s_1,n_1)\) 的一个子序列(不一定连续)。

注意到 \(m'\) 的上界很大,为 \(\dfrac{|s_1|\times n_1}{|s_2|}\),于是我们考虑二进制拆分的思想。设 \(l=\log_2\left(\dfrac{|s_1|\times n_1}{|s_2|}\right)\),表示 \(m'\) 的二进制最多有多少位。

\(m'=2^{p_1}+2^{p_2}+\dots+2^{p_k}\),把 \(conn(s_2,m')\) 看做由 \(conn(s_2,2^{p_1}),conn(s_2,2^{p_2}),\dots,conn(s_2,2^{p_k})\) 拼接而成,其中 \(p_i\le l\)

最后统计答案的时候,用尽量大的 \(2^{p_i}\) 在所用若干个 \(s_1\) 总长度不超过 \(|s_1|\times n_1\) 的情况下拼成 \(m'\)

考虑每一个 \(conn(s_2,2^j)\)。由于 \(conn(s_1,n_1)\) 是由很多个 \(s_1\) 重复拼接而成的,我们不妨设想一个字符串 \(s_1'=conn(s_1,+\infty)\),即令 \(s_1\) 重复无数次。

由于所用的 \(s_1'\) 长度与开始匹配的起点 \(i\)\(s_2\) 的重复次数 \(j\) 有关,我们设 \(f(i,j)\) 表示从 \(s_1'[i]\) 开始,要想生成 \(conn(s_2,2^j)\),需要用 \(s_1'\) 的最小连续长度。

即用 \(s_1'[i..i+f(i,j)-1]\) 能够生成 \(conn(s_2,2^j)\) 的最小 \(f(i,j)\)

\(s_1'\) 中,从第 \(i\) 位开始和从第 \(i+k|s_1|\) 位开始是等效的,所以 \(f(i,j)\)\(i\in[0,|s_1|)\)

考虑倍增的思想,我们先用尽量短的 \(s_1'\) 生成 \(conn(s_2,2^{j-1})\),再从当前位置开始用尽量短的 \(s_1'\) 生成另一个 \(conn(s_2,2^{j-1})\)

\[f(i,j)=f(i,j-1)+f[(i+f(i,j-1))\bmod|s_1|,j-1]. \]

初值 \(f(i,0)\) 可以暴力匹配 \(s_1'\)\(s_2\) 处理。

接下来对 DP 的阶段 \(j\) 进行拼接。

第一步,枚举起始位置 \(i\in[0,|s_1|)\)

第二步,用 \(p\) 记录当前位置,开始时 \(p=i\),从 \(l\)\(0\) 倒序循环枚举 \(j\),如果当前的 \(conn(s_1,n_1)\) 足够生成 \(conn(s_2,2^j)\),即 \(p+f(p\bmod|s_1|,j)\le|s_1|\times n_1\),那么累加答案,并且改变当前位置,即令 \(p\gets p+f(p\bmod|s_1|,j)\),直到无法再生成;

第三步,打擂台更新答案,枚举下一个 \(i\)

最后别忘了将答案除以 \(n_2\) 以得到真正的 \(m\)

代码

点击查看代码
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
const int N = 110;
int n1, n2, l1, l2, l;
long long f[N][33], ans, tmp, x;
string s1, s2;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    while (cin >> s2 >> n2 >> s1 >> n1) {
        l1 = s1.length(), l2 = s2.length();
        l = log2(l1 * n1 / l2);
        for (int i = 0; i < l1; ++i) {
            int p = i;
            f[i][0] = 0;
            for (int j = 0; j < l2; ++j) { //匹配s2的每一位
                int cnt = 0;
                while (s1[p] != s2[j]) {
                    if (++p == l1) p = 0;
                    if (++cnt >= l1) { //s1中匹配不上这一位
                        cout << 0 << '\n';
                        goto label; //goto语句, 直接跳到循环最后的continue
                    }
                }
                if (++p == l1) p = 0;
                f[i][0] += cnt + 1;
            }
        }
        for (int j = 1; j <= l; ++j)
            for (int i = 0; i < l1; ++i)
                f[i][j] = f[i][j - 1] + f[(i + f[i][j - 1]) % l1][j - 1];
        x = tmp = 0;
        for (int j = l; j >= 0; --j)
            if (x + f[x % l1][j] <= l1 * n1)
                x += f[x % l1][j], tmp += (1 << j);
        cout << tmp / n2 << '\n';
        label:
            continue;
    }
    
    return 0;
}

C. [USACO04DEC]Cleaning Shifts S(线段树优化 DP,离散化)

POJ 2376 | AcWing 295 | 码创未来

题意

给定 \(n\) 个区间,用 \(l_i,r_i(1\le i\le n)\) 表示,有一排 \(T\) 个格子,用区间(可以重叠)覆盖所有格子,求最小区间数。

\(1\le n\le25000,1\le T\le10^6\)

思路

\(f(i)\) 表示覆盖 \([1,i]\) 最少需要多少个区间。初值 \(f(0)=0,f(i)=+\infty(i\ne0)\)

将区间按 \(r\) 从小到大排序,枚举区间。

对于当前区间 \(i\),它能覆盖的范围为 \([l_i,r_i]\),所以需要之前覆盖的范围最少为 \([1,l_i-1]\),最多为 \([1,r_i-1]\),于是可以得出状态转移方程。

\[f(r_i)=\min_{l_i-1\le j\le r_i-1}\{f(j)\}+1. \]

对于取 min 的操作,我们用线段树来维护。数据范围较大,需要离散化(或许也可以不离散化)。

代码

点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 2.5e4 + 10;
const int INF = 0x3f3f3f3f;
int n, m, f[N << 2], ans = INF, raw[N << 2], cnt;

struct Node {
    int l, r;
    friend bool operator<(Node const &p, Node const &q) {
        return p.r < q.r;
    }
} a[N];

struct SegTree {
    #define lson (u << 1)
    #define rson (u << 1 | 1)
    struct Node {
        int l, r, minn;
    } tr[N << 4];
    inline void pushup(int u) { tr[u].minn = min(tr[lson].minn, tr[rson].minn); }
    void build(int u, int l, int r) {
        tr[u].l = l, tr[u].r = r;
        tr[u].minn = l ? INF : 0;
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(lson, l, mid);
        build(rson, mid + 1, r);
        // pushup(u);
    }
    void modify(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].minn = v;
            return;
        }
        int mid = (tr[u].l + tr[u].r) >> 1;
        if (x <= mid) modify(lson, x, v);
        else modify(rson, x, v);
        pushup(u);
        return;
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].minn;
        int res = INF;
        int mid = (tr[u].l + tr[u].r) >> 1;
        if (l <= mid) res = min(res, query(lson, l, r));
        if (r > mid) res = min(res, query(rson, l, r));
        return res;
    }
} t;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> m;
    raw[++cnt] = 1, raw[++cnt] = m;
    f(i, 1, n) {
        cin >> a[i].l >> a[i].r;
        raw[++cnt] = a[i].l, raw[++cnt] = a[i].r;
        raw[++cnt] = a[i].l + 1, raw[++cnt] = a[i].r + 1;
    }
    sort(a + 1, a + n + 1);
    sort(raw + 1, raw + cnt + 1);
    cnt = unique(raw + 1, raw + cnt + 1) - raw - 1;
    while (raw[cnt] > m) --cnt;
    memset(f, 0x3f, sizeof f);
    f[0] = 0;
    t.build(1, 0, cnt);
    f(i, 1, n) {
        a[i].l = lower_bound(raw + 1, raw + cnt + 1, a[i].l) - raw;
        a[i].r = lower_bound(raw + 1, raw + cnt + 1, a[i].r) - raw;
        int tmp = t.query(1, a[i].l - 1, a[i].r - 1) + 1;
        if (f[a[i].r] > tmp) {
            f[a[i].r] = tmp;
            t.modify(1, a[i].r, f[a[i].r]);
        }
    }
    if (f[cnt] == INF) cout << "-1\n";
    else cout << f[cnt] << '\n';
    
    return 0;
}

D. [USACO05DEC]Cleaning Shifts S(线段树优化 DP)

洛谷 P4644 | POJ 3171 | AcWing 296 | 码创未来

题意

给定 \(n\) 个区间,用 \(l_i,r_i(1\le i\le n)\) 表示,第 \(i\) 个区间有一个代价 \(c_i\),有一排 \(T\) 个格子,用这些区间(可以重叠)覆盖 \([L,R]\) 内的所有格子,求所需的最小代价。

\(1\le n\le10000,0\le L,R\le86399,L\le l_i\le r_i\le R,0\le c_i\le500000\)

思路

与上一题不同之处在于不需要离散化;每个区间有一定的代价;覆盖的范围为 \([L,R]\)

\(f(i)\) 表示覆盖 \([L,i]\) 所需的最小代价。初值 \(f(L-1)=0,f(i)=+\infty(L\le i\le R)\)

\[f(r_i)=\min_{l_i-1\le j\le r_i-1}\{f(j)\}+c_i. \]

用线段树维护区间最小值。答案为 \(f(R)\)

代码

点击查看代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
const int N = 1e4 + 10, M = 1e5 + 10;
int n, L, R;
long long f[M];

struct Cow {
	int a, b, c;
	bool operator<(Cow const &o) const {
		return b < o.b;
	}
} a[N];

namespace SegTree {
	#define lson (u << 1)
	#define rson (u << 1 | 1)
	struct Node {
		int l, r;
		long long x;
	} tr[M << 2];
	il void pushup(int u) { tr[u].x = min(tr[lson].x, tr[rson].x); }
	void build(int u, int l, int r) {
		tr[u].l = l, tr[u].r = r;
		if (l == r) return tr[u].x = f[l], void();
		int mid = (l + r) >> 1;
		build(lson, l, mid);
		build(rson, mid + 1, r);
		pushup(u);
		return;
	}
	void modify(int u, int x, long long y) {
		if (tr[u].l == x && tr[u].r == x) {
			tr[u].x = y;
			return;
		}
		// pushdown(u);
		int mid = (tr[u].l + tr[u].r) >> 1;
		if (x <= mid) modify(lson, x, y);
		else modify(rson, x, y);
		pushup(u);
		return;
	}
	long long query(int u, int l, int r) {
		if (l <= tr[u].l && tr[u].r <= r) {
			return tr[u].x;
		}
		long long res = 4557430888798830399LL; //0x3f3f3f3f3f3f3f3f
		int mid = (tr[u].l + tr[u].r) >> 1;
		if (l <= mid) res = min(res, query(lson, l, r));
		if (r > mid) res = min(res, query(rson, l, r));
		return res;
	}
}
using SegTree::build;
using SegTree::modify;
using SegTree::query;

signed main() {
	
	scanf("%d%d%d", &n, &L, &R);
	++L, ++R;
	memset(f, 0x3f, sizeof f);
	f[L - 1] = 0;
	f(i, 1, n) {
		scanf("%lld%lld%lld", &a[i].a, &a[i].b, &a[i].c);
		a[i].a = max(++a[i].a, L);
		a[i].b = min(++a[i].b, R);
	}
	sort(a + 1, a + n + 1);
	build(1, L - 1, R);
	f(i, 1, n) {
		f[a[i].b] = min(f[a[i].b], query(1, a[i].a - 1, a[i].b - 1) + a[i].c);
		modify(1, a[i].b, f[a[i].b]);
	}
	if (f[R] >= 4557430888798830399LL) puts("-1");
	else printf("%lld\n", f[R]);
	
	return 0;
}

*E. The Battle of Chibi(树状数组优化 DP,离散化)

洛谷 UVA12983 | AcWing 297 | 码创未来

题意

给定长度为 \(n\) 的数列 \(A\),求 \(A\) 有多少个长度为 \(m\) 的严格递增子序列。\(T\) 组测试数据。

对于每组测试数据,\(1\le m\le n\le1000,|A_i|\le10^9(1\le i\le n)\)

对于每个测试点,数据满足 \(1\le T\le100,\sum_{i=1}^Tn_i\times m_i\le10^7\)

思路

\(f(i,j)\) 表示在前 \(j\) 个数中,有多少个长度为 \(i\) 且以 \(A_j\) 为结尾的严格递增子序列。显然若 \(i>j\)\(f(i,j)=0\)。初值 \(f(0,0)=1\)

可以写出状态转移方程:

\[f(i,j)=\sum_{k<j\wedge a_k<a_j}f(i-1,k). \]

如果每次都要重新扫一遍 \(k\),那么代价是 \(O(n^3)\) 的(\(m,n\) 视为同阶),显然会喜提一个大大的 TLE。如何优化呢?

我们先把外层循环的 \(i\) 看做定值。对于每一个 \(j\),我们想知道的是满足 \(a_k<a_j\)\(f(i-1,k)\) 的和。

由于 \(j\) 从小到大枚举,所以 \(k<j\) 的条件自然就满足了。我们把 \(a_k\) 当做比较大小的关键码,将 \(f(i-1,k)\) 加入树状数组,每次询问所有小于 \(a_j\)\(a_k\) 的和。

由于 \(a_i\) 很大,所以需要进行离散化。我们把数组映射到区间 \([2,n+1]\) 上,把 \(-\infty\) 映射到 \(1\) 上。

当枚举到每一个 \(i\) 时,我们清空树状数组,然后向树状数组的位置 \(1\)(对应 \(-\infty\))上插入 \(f(i-1,0)\)\(1(i=1)\)\(0(i\ne1)\)

然后枚举 \(j\),需要进行两个操作(顺序任意):

  • 查询小于 \(a_j\) 的所有 \(a_k\)\(f(i-1,k)\) 的和,修改 \(f(i,j)\) 的值;
  • 向树状数组中 \(j\) 对应的位置 \(val_j\) 上插入 \(f(i-1,j)\)

答案为 \(\sum\limits_{i=m}^nf(m,i)\)

代码

点击查看代码
#include <iostream>
#include <cstring>
#include <unordered_map>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
const int N = 1e3 + 10;
const int MOD = 1e9 + 7;
const int INF = 0x3f3f3f3f;
int tt, n, m, a[N], f[N][N], raw[N], c[N], cnt, ans;
unordered_map<int, int> val;

il int lowbit(int x) { return x & (-x); }

void add(int x, int y) {
    while (x <= cnt) {
        c[x] += y;
        c[x] %= MOD;
        x += lowbit(x);
    }
    return;
}

int query(int x) {
    int res = 0;
    while (x >= 1) {
        res += c[x];
        res %= MOD;
        x -= lowbit(x);
    }
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> tt;
    f(x, 1, tt) {
        val.clear();
        ans = 0;
        cin >> n >> m;
        f(i, 1, n) cin >> a[i], raw[i] = a[i];
        raw[0] = -INF;
        sort(raw, raw + n + 1);
        cnt = unique(raw, raw + n + 1) - raw;
        f(i, 0, cnt - 1) val[raw[i]] = i + 1;
        // f[0][0] = 1;
        f(i, 1, m) {
            memset(c, 0, sizeof c);
            add(1, (bool)(i == 1));
            f(j, 1, n) {
                f[i][j] = query(val[a[j]] - 1);
                add(val[a[j]], f[i - 1][j]);
            }
        }
        f(i, m, n) ans += f[m][i], ans %= MOD;
        cout << "Case #" << x << ": " << ans << '\n';
    }
    
    return 0;
}

*F. Cut The Sequence(单调队列优化 DP)

POJ 3017 | AcWing 299 | 码创未来

TO DO...

G. Bribing FIPA(树上背包 DP)

洛谷 UVA1222 | AcWing 324 | 码创未来

题意

给定一个 \(n\) 个节点的森林和非负整数 \(m(0\le m\le n)\),节点 \(i\) 有费用 \(a_i\)。每个节点的价值即为以它为根的子树大小。

要求选定若干节点,使得价值总和 \(\ge m\)。并且如果选定了一个子树,则这个子树中的点不能再被选定。

求选定节点的费用和的最小值。

\(1\le n\le200,0\le m\le n\)

思路

选定一个节点,就相当于选定了它下面的所有点。

\(f(u,i)\) 表示在以 \(u\) 为根的子树中,选定若干节点使价值总和 \(\ge i\) 的最小费用和。

\(v\)\(u\) 的儿子,那么状态转移方程为:

\[f(u,i)=\min_{0\le j\le i}\{f(u,j)+f(v,i-j)\}. \]

初值 \(f(u,i)=+\infty,f(u,0)=0\)

由于一个 \(f(u,i)\) 只能由一个 \(v\) 更新,所以要倒序枚举 \(i\)。这一点与背包问题很相似。

为了把森林变成一棵树,我们建立一个超级根节点 \(0\)。令 \(a_0=+\infty\)。答案为 \(\min\limits_{m\le i\le n}\{f(0,i)\}\)

代码

点击查看代码
#include <cstdio>
#include <sstream>
#include <cstring>
#include <unordered_map>
#include <string>
#include <vector>
#include <bitset>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
const int N = 210;
const int INF = 0x3f3f3f3f;
int n, m, c, a[N], siz[N], f[N][N], ans;
char s[22000];
unordered_map<string, int> mp;
vector<int> e[N];
bitset<N> isRoot;

int dfs(int u) {
    memset(f[u], 0x3f, sizeof f[u]);
    f[u][0] = 0;
    int siz = 1;
    for (auto v: e[u]) {
        siz += dfs(v);
        g(i, n, 0) f(j, 0, i)
            f[u][i] = min(f[u][i], f[u][j] + f[v][i - j]);
    }
    f[u][siz] = min(f[u][siz], a[u]);
    return siz;
}

signed main() {
    
    while (true) {
        fgets(s, sizeof s, stdin); //读入整行存到字符串中
        if (s[0] == '#') break;
        sscanf(s, "%d%d", &n, &m); //从字符串中读入数据
        c = 0;
        mp.clear();
        f(i, 0, n) e[i].clear();
        isRoot.set(); //全部设为1
        f(i, 1, n) {
            int x, y, tmp;
            scanf("%s %d", s, &tmp);
            if (mp.find(s) == mp.end()) mp[s] = ++c;
            x = mp[s];
            a[x] = tmp;
            fgets(s, sizeof s, stdin); //读入整行
            stringstream ss(s); //字符串流
            string str;
            while (ss >> str) { //在字符串流中读入字符串
                if (mp.find(str) == mp.end()) mp[str] = ++c;
                y = mp[str];
                isRoot[y] = 0;
                e[x].push_back(y); //建边(指向儿子)
            }
        }
        f(i, 1, n) if (isRoot[i]) e[0].push_back(i); //超级根节点
        a[0] = INF;
        dfs(0);
        ans = INF;
        f(i, m, n) ans = min(ans, f[0][i]);
        printf("%d\n", ans);
    }
    
    return 0;
}

H. Computer(换根树上 DP)

AcWing 325 | 码创未来

题意

给定一棵 \(n\) 个节点的树,边有权值 \(l\),求每个节点到其他节点的最远距离。

\(2\le n\le100000,\sum l\le10^9\)

思路

(简单题,说得挺多,实际上没什么。。)

对于一个节点,这条最长的路径可能是向下到子树中的,也可能是向上经过父亲的。

我们设节点 \(i\) 向下到某一个叶子的最长路径为 \(dlf_i\)(distance to leaf),向上的最长路径为 \(dup_i\)

那么节点 \(i\) 的答案即为 \(\max\{dlf_i,dup_i\}\)

对于 \(dlf\),我们用一次 DFS 容易求得。具体来说,如果 \(v\)\(u\) 的儿子,它们之间的边权为 \(w_{u,v}\),那么在 DFS 回溯之后更新 \(dlf_u\)

\[dlf_u=\max\{dlf_v+w_{u,v}\}. \]

对于 \(dup\),我们再用一次 DFS 也可以求得。如果 \(v\)\(u\) 的儿子,它们之间的边权为 \(w_{u,v}\),那么由 \(u\) 更新 \(v\),讨论从 \(u\) 向上还是向下:

\[dup_v=\max\{dup_u,dlf_u\}+w_{u,v}. \]

但是!!!我们发现一个重要的 bug:如果长度为 \(dlf_u\) 的这条路径经过 \(v\) 怎么办?这时由 \(dlf_u\) 更新 \(dup_v\) 不是重复经过 \(v\) 了吗??

所以我们再保存一个由节点向下到叶子的所有路径的次大值,并且保存最大值是从哪个儿子更新来的

\(dlf_{i,0}\) 表示由 \(i\) 向下到叶子的所有路径的最大值,\(dlf_{i,1}\) 表示由 \(i\) 向下到叶子的所有路径的次大值,\(son_i\) 表示更新 \(dlf_{i,0}\)\(i\) 的那个儿子。

处理 \(dlf\) 的过程与上面类似,分别与当前的 \(dlf_{u,0}\) 和当前的 \(dlf_{u,1}\) 比较即可,过程中保存 \(son_u\)\(dup_v\) 的状态转移要考虑 \(v\) 是否等于 \(son_u\)

答案为 \(\max\{dlf_{i,0},dup_i\}\)

代码

点击查看代码
#include <iostream>
#include <cstring>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 1e5 + 10;
int n, dlf[N][2], son[N], dup[N];

struct Edge {
    int to, nxt, val;
} e[N << 1];
int head[N], cnt;
inline void add(int from, int to, int val) {
    e[++cnt].to = to, e[cnt].nxt = head[from], e[cnt].val = val, head[from] = cnt;
    return;
}

void dfs1(int u, int fa) {
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to, w = e[i].val;
        if (v == fa) continue;
        dfs1(v, u);
        if (dlf[v][0] + w > dlf[u][0]) {
            dlf[u][1] = dlf[u][0];
            dlf[u][0] = dlf[v][0] + w, son[u] = v;
        } else if (dlf[v][0] + w > dlf[u][1]) {
            dlf[u][1] = dlf[v][0] + w;
        }
    }
}

void dfs2(int u, int fa) {
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to, w = e[i].val;
        if (v == fa) continue;
        dup[v] = max(dup[u], (son[u] == v) ? dlf[u][1] : dlf[u][0]) + w;
        dfs2(v, u);
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    while (cin >> n) {
        memset(dlf, 0, sizeof dlf);
        memset(dup, 0, sizeof dup);
        memset(head, 0, sizeof head);
        cnt = 0;
        f(i, 2, n) {
            int y, l;
            cin >> y >> l;
            add(i, y, l), add(y, i, l);
        }
        dfs1(1, 1);
        dfs2(1, -1);
        f(i, 1, n) cout << max(dlf[i][0], dup[i]) << '\n';
    }
    
    return 0;
}

*I. [HNOI2011]XOR和路径(有后效性 DP,高斯消元,期望)

洛谷 P3211 | AcWing 326 | 码创未来

TO DO...

J. Fence Obstacle Course(线段树优化 DP)

POJ 2374 | AcWing 329 | 码创未来

题意

农夫约翰为他的奶牛们建造了一个围栏障碍训练场,以供奶牛们玩耍。

训练场由 \(n\) 个不同长度的围栏组成,每个围栏都与 \(x\) 轴平行,并且第 \(i\) 个围栏的 \(y\) 坐标为 \(i\)\(x\) 坐标覆盖 \([a_i,b_i]\)

训练场的出口位于原点 \((0,0)\),起点位于 \((S,n)\)

这些牛会从起点处开始向下走,当它们碰到围栏时会选择沿着围栏向左或向右走,走到围栏端点时继续往下走,按照此种走法一直走到出口为止。

求出这些牛从开始到结束,行走的水平距离的最小值。

\(1\le n\le30000,−10^5\le S\le10^5,−10^5\le a_i,b_i\le10^5\)

思路

为了方便转移,我们将从上往下走改为从下往上走,从 \((0,0)\) 最终走到 \((S,n)\)。如图所示。

\(f(i,j)\) 表示到第 \(i\) 层栅栏的左/右端点时的答案,其中 \(1\le i<n,j\in\{0,1\}\)

初值:\(f(0,0)=f(0,1)=0\)。令 \(a_0=b_0=0\)

\(lst(i)\) 表示上一个覆盖直线 \(x=i\) 的栅栏编号。

那么状态转移方程为:

\[\begin{aligned} &f(i,0)=\min\{f(lst(a_i),0)+|a_i-a_{lst(a_i)}|,f(lst(a_i),1)+|a_i-b_{lst(a_i)}|\},\\ &f(i,1)=\min\{f(lst(b_i),0)+|b_i-a_{lst(b_i)}|,f(lst(b_i),1)+|b_i-b_{lst(b_i)}|\}. \end{aligned} \]

答案为 \(\min\{f(n-1,0)+|a_{n-1}-S|,f(n-1,1)+|b_{n-1}-S|\}\)

还有一个问题:如何快速求出 \(lst_i\)

我们需要一个支持区间覆盖、单点查询的数据结构,线段树标记下传可以很好地解决。

代码

点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 5e4 + 10, M = 3e5 + 10;
int n, s, f[N][2], a[N], b[N];

inline int abs(int x) { return __builtin_abs(x); }

struct SegTree { //区间覆盖, 单点查询
    #define lson (u << 1)
    #define rson (u << 1 | 1)
    struct Node {
        int l, r, x;
    } tr[M << 2];
    inline void pushdown(int u) {
        if (~tr[u].x) tr[lson].x = tr[rson].x = tr[u].x, tr[u].x = -1;
        return;
    }
    void build(int u, int l, int r) {
        tr[u].l = l, tr[u].r = r;
        if (l == r) return;
        else tr[u].x = -1;
        int mid = (l + r) >> 1;
        build(lson, l, mid);
        build(rson, mid + 1, r);
        return;
    }
    void modify(int u, int l, int r, int x) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].x = x, void();
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) >> 1;
        if (l <= mid) modify(lson, l, r, x);
        if (r > mid) modify(rson, l, r, x);
        return;
    }
    int query(int u, int x) {
        if (tr[u].l == x && tr[u].r == x) return tr[u].x;
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) >> 1;
        if (x <= mid) return query(lson, x);
        else return query(rson, x);
    }
} t;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n >> s;
    s += 1e5;
    t.build(1, 0, 2e5);
    // t.modify(1, 0, 2e5, 0);
    a[0] = 1e5, b[0] = 1e5;
    // f[0][0] = f[0][1] = 0;
    f(i, 1, n) {
        cin >> a[i] >> b[i];
        a[i] += 1e5, b[i] += 1e5;
        int j = t.query(1, a[i]);
        f[i][0] = min(f[j][0] + abs(a[i] - a[j]), f[j][1] + abs(a[i] - b[j]));
        j = t.query(1, b[i]);
        f[i][1] = min(f[j][0] + abs(b[i] - a[j]), f[j][1] + abs(b[i] - b[j]));
        t.modify(1, a[i], b[i], i);
    }
    cout << min(f[n][0] + abs(a[n] - s), f[n][1] + abs(b[n] - s)) << '\n';
    
    return 0;
}

*K. [USACO09OPEN]Tower of Hay G(单调队列优化 DP)

洛谷 P4954 | AcWing 331 | 码创未来

题意

一共有 \(n\) 大包的干草(从 \(1\)\(n\) 编号),第 \(i\) 包干草有一个宽度 \(w_i\)。所有的干草包的厚度和高度都为 \(1\)

Bessie 必须利用所有 \(n\) 包干草来建立起干草堆。具体来说,她可以在一层中放若干包干草,但是这些干草只能紧挨着放在一起,并且总宽度不超过下面一层(最下面一层的宽度无限制)。她持续像这样堆放干草,直到所有的草包都被安置完成。

她必须按照从 \(1\)\(n\) 的顺序堆放干草包。

Bessie 的目标是建立起最高的干草堆。求出最高的干草堆的高度。

\(1\le N\le100000,1\le w_i\le10000\)

思路

由于从下到上搭干草堆不好维护,我们考虑从上到下搭干草堆。那么应依次读入 \(n\)\(1\) 号的干草包。

猜想:如果使底层宽度最小,那么一定可以构造出一种层数最高的方案

证明:对于当前的所有干草,任取一个能使层数最高的方案,设有 \(C_A\) 层,把其中从下往上每一层最大的块编号记为 \(A_i\);任取一个能使底边最短的方案,设有 \(C_B\) 层,把其中从下往上每一层最大的块编号记为 \(B_i\)。显然 \(A_1\ge B_1,A_{C_B}\le B_{C_B}\),这说明至少存在一个 \(k\in(1,C_B)\),满足 \(A_{k-1}\ge B_{k-1}\)\(A_k\le B_k\)。也就是说,方案 \(A\)\(k\) 层完全被方案 \(B\)\(k\) 层包含。构造一个新方案,第 \(k\) 层往上按方案 \(A\),往下按方案 \(B\),两边都不要的块放中间当第 \(k\) 层。新方案的层数与 \(A\) 相同,而底边长度与 \(B\) 相同。证毕。(proof by zkw)(引自 USACO 2009 Open 干草塔Tower of Hay 解题报告 - Lazycal - 博客园

\(f(i)\) 表示用第 \(1\)\(i\) 包干草最多能搭多少层。初值 \(f(0)=0\),答案为 \(f(n)\)

\(g(i)\) 表示从上到下第 \(i\) 层的最小宽度。

状态转移方程:

\[f(i)=\max_{\begin{matrix}1\le j<i\\w_{j+1}+\dots+w_i>g(j)\end{matrix}}\{f(j)\}+1 \]

含义是:枚举 \(j\),把 \(j+1\)\(i\) 的这些干草放到当前这一层(如果能放的话)。只是暴力枚举 \(j\) 的话,复杂度是 \(O(n^2)\) 的,所以我们要进一步优化。

\(w\) 的前缀和数组为 \(s\)。条件可以表示为:\(s_i-s_j>g(j)\)。注意到如果移项变为 \(g(j)+s_j<s_i\),那么要维护的多项式只与 \(j\) 有关,并且随着 \(j\) 递增, \(g(j)\)\(s_j\) 都单调递增。所以为了找到最大的(因为要更新 \(g(j)\))满足上述不等式的 \(j\),我们维护一个 \(g(j)+s_j\) 单调递增,下标单调递增的单调队列。那么每次弹出队首直至不满足条件最后一个满足条件的队首就是满足条件的最大的 \(j\)。用 \(f(j)+1\) 更新 \(f(i)\),并且用 \(s_i-s_j\) 更新 \(g(j)\)

总之这道题的特殊之处在于不是直接取队首,而是取最后一个满足条件的队首。

代码

点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
const int N = 1e5 + 10;
int n, w[N], s[N], q[N], h, t, f[N], g[N], ans;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    
    cin >> n;
    g(i, n, 1) cin >> w[i];
    h = t = 1;
    f[0] = 0;
    f(i, 1, n) {
        s[i] = s[i - 1] + w[i];
        while (h <= t && g[q[h]] + s[q[h]] <= s[i]) ++h;
        int j = q[h - 1];
        f[i] = f[j] + 1;
        ans = max(ans, f[i]);
        g[i] = s[i] - s[j];
        while (h <= t && g[q[t]] + s[q[t]] >= g[i] + s[i]) --t;
        q[++t] = i;
    }
    cout << ans << '\n';
    
    return 0;
}
posted @ 2022-11-15 17:44  f2021ljh  阅读(78)  评论(0)    收藏  举报