第四周专题(8.1-8.7):动态规划(DP)(14/15)

第四周专题(8.1-8.7):动态规划(DP)

比赛链接:004-2022-08-1

线性DP

E题 最长公共子序列

求出 \(A,B\) 的最长公共子序列长度,并求出该长度的公共串数量。

\(|A|,|B|\leq 5*10^3\)

对于前一个问题,显然有:

\[f_{i,j}=\max\begin{cases}f_{i-1,j} \\f_{i,j-1}\\f_{i-1,j-1}+1\end{cases} \]

对于后者,我们改为计数,判断最长公共子序列究竟是从哪转移过来的。

  1. \(f_{i,j}=f_{i-1,j-1}+1\) 时,\(g_{i,j}\) 需要加上 \(g_{i-1,j-1}\)
  2. \(f_{i,j}=f_{i-1,j}\) 时,需要加上 \(g_{i-1,j}\)
  3. \(f_{i,j}=f_{i,j-1}\) 时,需要加上 \(g_{i,j-1}\)
  4. 如果同时执行了 \(2,3\),那么根据容斥,还需要减掉 \(g_{i-1,j-1}\)(这一步只需要看 \(f_{i-1,j-1}\)\(f_{i,j}\) 是否相等就行了)。
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e8, N = 5010;
int n, m, f[2][N], r[2][N];
char s1[N], s2[N];
int main()
{
    scanf("%s%s", s1 + 1, s2 + 1);
    n = strlen(s1 + 1) - 1, m = strlen(s2 + 1) - 1;
    for (int k = 0; k <= m; k++)
        r[0][k] = 1;
    r[1][0] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            f[i & 1][j] = max(f[(i - 1) & 1][j], f[i & 1][j - 1]);
            if (s1[i] == s2[j])
                f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - 1] + 1);
            int k = f[i & 1][j];
            r[i & 1][j] = 0;
            if (s1[i] == s2[j] && f[(i - 1) & 1][j - 1] == k - 1)
                r[i & 1][j] += r[(i - 1) & 1][j - 1];
            //
            if (f[(i - 1) & 1][j] == k) r[i & 1][j] += r[(i - 1) & 1][j];
            if (f[i & 1][j - 1] == k) r[i & 1][j] += r[i & 1][j - 1];
            if (f[(i - 1) & 1][j - 1] == k) r[i & 1][j] -= r[(i - 1) & 1][j - 1];
            //mod
            r[i & 1][j] = (r[i & 1][j] + mod) % mod;
        }
    }
    printf("%d\n%d\n", f[n & 1][m], r[n & 1][m]);
    return 0;
}

期望DP

I题 绿豆蛙的归属

给定一张 \(n\)\(m\) 边的(边带权)有向无环图,起点和终点分别为 \(1,n\)。从任何点出发都可以到达终点,且起点可到达任何点。

我们从起点出发,走向终点,当到达一个顶点时,如果该节点有 \(k\) 条出边,那么我们会等概率的随机选择一条。求出起点到达终点的路径总长度的期望。

\(n\leq 10^5,1\leq w_i\leq 10^9\)

\(f_i\) 为起点到 \(i\) 的期望长度的话,递推有些不方便,我们不妨换一下,记 \(f_i\) 为点到 \(n\) 的期望长度,那么有 \(f_n=0\),且答案就是 \(f_1\)

考虑转移方程:点 \(x\) 可能是由其的所有后缀 \(y\) 转移过来且等概率,那么就有:

\[f_x=\frac{\sum_{y\in S}(f_y+w)}{k} \]

#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = N << 1;
//
int tot = 0, ver[M], edge[M], Head[N], Next[M], od[N];
void addEdge(int x, int y, int z) {
    ver[++tot] = y, edge[tot] = z;
    Next[tot] = Head[x], Head[x] = tot;
}
int n, m, vis[N];
double dp[N];
double dfs(int x) {
    if (vis[x]) return dp[x];
    vis[x] = 1;
    for (int i = Head[x]; i; i = Next[i])
        dp[x] += dfs(ver[i]) + edge[i];
    return dp[x] /= od[x];
}
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int x, y, z;
        cin >> x >> y >> z;
        addEdge(x, y, z), ++od[x];
    }
    dp[n] = 0, vis[n] = 1;
    printf("%.2lf", dfs(1));
    return 0;
}

H题 Tyvj1952 Easy

给定一个 xo 串,该串的得分根据 combo 的总和来计算,每个 combo 都是一个极大连续o串,值为长度的平方。(打个比方,ooxxxxooooxxx 有两个 combo,分别长度为 2, 4,得分为 \(2^2+4^2=20\))。

给定一个由 \(o,x,?\) 组成的串,问号位置随机生成 o 或者 x(等概率,一半一半),求这个串的得分期望。

\(n\leq 3*10^5\)

我们先尝试用 \(g_i\) 表示以 \(s_i\) 结尾的 combo 串的期望长度,那么有

\[g_i=\begin{cases}0 & s_i=\text{x} \\g_{i-1}+1&s_i=\text{o} \\\frac{g_{i-1}+1}{2} & s_i=\text{?} \end{cases} \]

那么我们继续考虑,令 \(f_i\) 表示以 \([1,i]\) 上的串的期望长度。

  1. \(s_i=\text{x}\) 时,显然有 \(f_i=f_{i-1}\)

  2. \(s_i=\text{o}\) 时,我们可以考虑扩展答案,假定前面的期望 combo长度为 \(k\),那么 \(g_i=k+1\),那么

    \[f_i=f_{i-1}-k^2+(k+1)^2=f_{i-1}+2k+1 \]

  3. \(s_i=\text{?}\) 时,等概率转移,那么得 \(f_i=f_{i-1}+\frac{2k+1}{2}\)

#include<bits/stdc++.h>
using namespace std;
const int N = 300010;
int n;
char s[N];
double f[N], g[N];
int main() {
    scanf("%d%s", &n, s + 1);
    for (int i = 1; i <= n; ++i) {
        if (s[i] == 'x')
            g[i] = 0, f[i] = f[i - 1];
        else if (s[i] == 'o')
            g[i] = g[i - 1] + 1, f[i] = f[i - 1] + 2 * g[i - 1] + 1;
        else {
            g[i] = 0.5 * g[i - 1] + 0.5;
            f[i] = f[i - 1] + g[i - 1] + 0.5;
        }
    }
    printf("%.4f\n", f[n]);
    return 0;
}

B题 奖励关

给定 \(n\) 个宝物,第 \(i\) 个宝物的价值为 \(p_i\)

接下来连续 \(k\) 次掉落宝物,每次都会等概率的掉落某一种宝物,你需要决定是否选取(采取最优策略),使得最后获得的宝物的价值最大, 求最大值的数学期望。

本题的限制条件如下:

  1. 每件宝物拥有其独特的选取条件,类似:你在选取该宝物前,必须曾选过了某些宝物
  2. 一些宝物的价值是负数,但是为了凑齐选取条件,你可能不得不去选一下它

\(k\leq 100,n\leq 15,|p_i|\leq 10^6\)

对于当前选取过的宝物的状态,我们直接状压,用一个 int 存一下就行,范围在 \([0,2^{15})\) 内。(为了方便,我们将所有宝物从 0 开始计数)

我们记 \(dp_{i,j}\) 为当前状态为 \(i\),已经进行了 \(j\) 次选择的条件下,剩下来 \(k-j\) 次所能获得的宝物价值的最大值的数学期望(初始状态下 \(dp_{*,k}=0\))。

每种宝物的掉落是等概率的,所以我们只需要分别算出掉落某种宝物的期望所得值,最后总的除以一下 \(n\) 即可。那么每次掉落宝物 \(t\),我们都有如下选择:

  1. 不选,那么需要加上 \(dp_{i,j+1}\)
  2. 选,加上 \(dp_{i|(2^t),j+1}+p_k\)

选不了的时候仅能执行 1,可以选的时候:

  1. 该物品之前选过了,那么价值大于等于 0 的再选一次,不然不选
  2. 该物品之前没选过,价值大于等于 0 的直接选,小于 0 的比较一下两者的最大值

总的代码如下:

for (int k = 0; k < n; ++k) {
    if ((i | s[k]) != i) {
        dp[i][j] += dp[i][j + 1];
        continue;
    }
    if ((i >> k) & 1) {
        if (p[k] >= 0) dp[i][j] += dp[i][j + 1] + p[k];
        else dp[i][j] += dp[i][j + 1];
    }
    else {
        if (p[k] >= 0) dp[i][j] += dp[i | (1 << k)][j + 1] + p[k];
        else dp[i][j] += max(dp[i | (1 << k)][j + 1] + p[k], dp[i][j + 1]);
    }
}

不过实际上,我们其实可以把这一大堆分类讨论直接优化掉,仅区分能不能选,是否更优直接交给 max 来处理。

for (int k = 0; k < n; ++k) {
    if ((i | s[k]) != i)
        dp[i][j] += dp[i][j + 1];
    else
        dp[i][j] += max(dp[i][j + 1], dp[i | (1 << k)][j + 1] + p[k]);
}

外层的状态数一共有 \(O(k2^n)\) 级别,内部还有 \(O(n)\) 的转移,总复杂度 \(O(nk2^n)\)

#include<bits/stdc++.h>
using namespace std;
const int K = 110;
int d, n, s[16], p[16];
double dp[1 << 16][K];
int main()
{
    //read & init
    cin >> d >> n;
    for (int i = 0, x; i < n; ++i) {
        cin >> p[i];
        while (true) {
            cin >> x;
            if (x == 0) break;
            s[i] |= 1 << (x - 1);
        }
    }
    //dp
    for (int j = d - 1; j >= 0; j--)
        for (int i = 0; i < (1 << n); ++i) {
            for (int k = 0; k < n; ++k) {
                if ((i | s[k]) != i)
                    dp[i][j] += dp[i][j + 1];
                else
                    dp[i][j] += max(dp[i][j + 1], dp[i | (1 << k)][j + 1] + p[k]);
            }
            dp[i][j] /= n;
        }
    printf("%.6f\n", dp[0][0]);
    return 0;
}

数位DP

推荐一手这个博客:数字组成的奥妙——数位dp

F题 count 数字计数

给定区间 \([l,r]\),求出区间中的所有整数中,每个数码出现了多少次。

\(1\leq l\leq r\leq 10^{12}\)

这题可以 DP 做,但是我觉得分别算贡献要方便一些(我不知道咋说了,直接看代码吧)。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL mul[20], b[20];
LL calc(int H, int L) {
    LL res = 0;
    for (int i = H; i >= L; i--)
        res = res * 10 + b[i];
    return res;
}
LL solve0(LL val) {
    int tot = 0;
    for (LL v = val; v; v /= 10) b[++tot] = v % 10;
    LL res = 0;
    for (int i = 1; i < tot; ++i) {
        if (b[i])
            res += (calc(tot, i + 1)) * mul[i - 1];
        else
            res += (calc(tot, i + 1) - 1) * mul[i - 1] + calc(i - 1, 1) + 1;
    }
    return res + 1;
}
LL solve(LL val, int x) {
    int tot = 0;
    for (LL v = val; v; v /= 10) b[++tot] = v % 10;
    LL res = 0;
    for (int i = 1; i <= tot; ++i) {
        if (x < b[i]) res += (calc(tot, i + 1) + 1) * mul[i - 1];
        else if (x == b[i])
            res += calc(tot, i + 1) * mul[i - 1] + calc(i - 1, 1) + 1;
        else
            res += calc(tot, i + 1) * mul[i - 1];
    }
    return res;
}
int main() {
    mul[0] = 1;
    for (int i = 1; i <= 15; ++i)
        mul[i] = mul[i - 1] * 10;
    LL L, R;
    cin >> L >> R;
    cout << solve0(R) - solve0(L - 1);
    for (int i = 1; i < 10; ++i)
        cout << " " << solve(R, i) - solve(L - 1, i);
    return 0;
}

G题 windy 数

对于一个不含前导零,且相邻两个数字之差至少为 2 的正整数被称为 windy 数,求出区间 \([l,r]\) 内有多少 windy 数。(个位数也是 windy 数)

\(1\leq l\leq r \leq 2*10^9\)

一个带了前导零的数位 DP 板子,状态考虑了 \((pos,pre,st)\) 三个(分别代表当前位置,前一个数的值,以及目前状态(是否还在保持 windy 的性质))。采用了记忆化搜索的形式,可以尝试看代码理解一下,不行就去看上面的博客。

#include <bits/stdc++.h>
using namespace std;
const int N = 12;
int bt[N], f[N][10][2];
int dfs(int pos, int pre, int st, int limit, int lead) {
    if (pos == 0) return st == 1;
    if (!limit && !lead && f[pos][pre][st] != -1) return f[pos][pre][st];
    int up = (limit ? bt[pos] : 9), sum = 0;
    for (int x = 0; x <= up; ++x) {
        int nst = 1, nlead = 0;
        if (lead) nlead = x == 0;
        else nst = st && abs(x - pre) >= 2;
       	sum += dfs(pos - 1, x, nst, limit && x == up, nlead);
    }
    if (!limit && !lead && f[pos][pre][st] == -1)
        f[pos][pre][st] = sum;
    return sum;
}
int dp(int x) {
    memset(f, -1, sizeof(f));
    int tot = 0;
    for (; x; x /= 10) bt[++tot] = x % 10;
    return dfs(tot, 0, 1, 1, 1);
}
int main() {
    int l, r;
    scanf("%d%d", &l, &r);
    printf("%d\n", dp(r) - dp(l - 1));
    return 0;
}

单调队列/斜率优化

本专题需要掌握单调队列和斜率优化两个知识点,建议hxd们先单独研究一下A题题解(指的是洛谷上面的详细斜率优化入门)后在开始看下面题目。

A题 玩具装箱toy

本题的朴素动态规划方程如下:

\[dp_i=\min\limits_{0\leq j<i}\{dp_j+(i-j-1+\sum\limits_{k=j+1}^{i}C_k-L)^2\} \]

我们定义 \(s_i=\sum\limits_{k=1}^iC_i\),那么有

\[dp_i=\min\limits_{0\leq j<i}\{dp_j+(i-j-1+s_i-s_j-L)^2\} \]

定义 \(a_i=s_i+i,b_i=s_i+i+L+1\),那么有

\[dp_i=\min\limits_{0\leq j<i}\{dp_j+(a_i-b_j)^2\} \\=\min\limits_{0\leq j<i}\{dp_j+a_i^2+b_j^2-2a_ib_j\} \]

接下来,将其转化为 \(y=kx+b\) 形式:

\[dp_i=dp_j+a_i^2+b_j^2-2a_ib_j \\ dp_j+b_j^2=2a_ib_j+dp_i-a_i^2 \]

\(y=dp_j+j^2,k=2a_i,b=dp_i-a^2\),可知 \(k\) 单调增,目标是让 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 50010;
int n, L;
LL sum[N], dp[N];
inline LL a(int i) { return sum[i] + i; }
inline LL b(int i) { return a(i) + L + 1; }
inline LL X(int i) { return b(i); }
inline LL Y(int i) { return dp[i] + b(i) * b(i); }
inline double slope(int i, int j) {
    double dy = Y(i) - Y(j), dx = X(i) - X(j);
    return dy / dx;
}
deque<int> q;
int main()
{
    cin >> n >> L;
    for (int i = 1; i <= n; i++) {
        cin >> sum[i];
        sum[i] += sum[i - 1];
    }
    q.push_back(0);
    int tot = 1;
    for (int i = 1; i <= n; i++) {
        //delete
        while (tot > 1 && slope(q[0], q[1]) < 2 * a(i))
            q.pop_front(), tot--;
        //dp
        dp[i] = dp[q[0]] + (a(i) - b(q[0])) * (a(i) - b(q[0]));
        //update
        while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
            q.pop_back(), tot--;
        //insert
        q.push_back(i), tot++;
    }
    cout << dp[n] << endl;
    return 0;
}

J题 序列分割

写完上面那些斜率优化后,我们欣喜的发现本题的难度并不在于DP的优化,而是想出朴素方程(qaq)。

我们考虑 \((a,b,c)\) 三个数,如果先分成 \((a,b),(c)\) 然后再分成三个单独段,那么所得值为 \(c(a+b)+ab=ab+ac+bc\),巧合(也不是很巧合)的是,先分成 \((a),(b,c)\) 的话,所得值是 \(a(b+c)+bc=ab+ac+bc\),也是一样的。

我们列出朴素DP方程,记 \(dp_{i,j}\) 为前 \(i\) 个数分成 \(k\) 段所得的最大值,那么有

\[dp_{i,k}=\max\limits_{1\leq j<i}\{dp_{j,k-1}+s_j(s_i-s_j)\} \]

朴素复杂度是 \(O(n^2k)\),需要消去一维。

形式转换,如下:

\[dp_{i,k}=dp_{j,k-1}+s_is_j-s_j^2 \\ s_j^2-dp_{j,k-1}=s_is_j-dp_{i,k} \\ \begin{cases} y=s_j^2-dp_{j,k-1}\\k=s_i\\b=-dp_{i,k} \end{cases} \]

\(k\) 单调增,目标是使得 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。

本题有一些注意点:

  1. 往常,我们会往单调队列里面扔一个初始值 0,但是本题不行(注意到上面的 j 的取值了吗?应该是从 1 开始),所以我们要先扔一个 1 进去,然后从 2 开始慢慢 DP
  2. 这题卡 STL,所以单调队列得手写(其实没啥不方便的)
  3. 洛谷上这题得输出方案,所以得注意一下空间的优化(例如 DP 数组就尽量滚动一下)
#include<bits/stdc++.h>
using namespace std;
int read() {
    int x = 0;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar());
    for (;  isdigit(ch); ch = getchar())
        x = x * 10 + ch - '0';
    return x;
}
typedef long long LL;
const int N = 100010, K = 210;
int n, k, tk, ans[N][K];
LL s[N], dp[N][K];
inline LL X(int i) { return s[i]; }
inline LL Y(int i) { return s[i] * s[i] - dp[i][tk - 1]; }
inline double slope(int i, int j) {
    if (X(i) == X(j)) return 1e30;
    double dy = Y(i) - Y(j), dx = X(i) - X(j);
    return dy / dx;
}
//
int Head, Tail, q[N];
int main()
{
    //read
    n = read(), k = read();
    for (int i = 1; i <= n; i++)
        s[i] = read(), s[i] += s[i - 1];
    //dp
    for (tk = 2; tk <= k + 1; ++tk) {
        Head = Tail = 1, q[1] = 1;
        for (int i = 2; i <= n; i++) {
            //delete
            while (Head < Tail && slope(q[Head], q[Head + 1]) < s[i])
                ++Head;
            //dp
            dp[i][tk] = dp[q[Head]][tk - 1] + s[q[Head]] * (s[i] - s[q[Head]]);
            ans[i][tk] = q[Head];
            //update
            while (Head < Tail && slope(i, q[Tail - 1]) < slope(q[Tail - 1], q[Tail]))
                --Tail;
            //insert
            q[++Tail] = i;
        }
    }
    cout << dp[n][k + 1] << endl;
    //
    vector<int> res;
    for (int i = n, j = k + 1; j >= 2; j--) {
        res.push_back(ans[i][j]);
        i = ans[i][j];
    }
    sort(res.begin(), res.end());
    for (int x : res) cout << x << " ";
    return 0;
}

K题 小P的牧场

不难推出本题的普通方程:

\[dp_i=a_i+\max\limits_{0\leq j<i}\{dp_j+\sum\limits_{k=j+1}^ib_k(i-k)\} \]

我们维护一下 \(c_i=ib_i\),随后对 \(\{b_n\},\{c_n\}\) 维护一下前缀和,那么方程就变为了

\[dp_i=a_i+\max\limits_{0\leq j<i}\{dp_j+i(b_i-b_j)-(c_i-c_j)\} \]

移项转换,得

\[dp_i=a_i+dp_j+ib_i-ib_j+c_j-c_i \\ dp_j+c_j=ib_j+dp_i-a_i-ib_i+c_i \\ y=dp_j+c_j,k=i,b=dp_i-a_i-ib_i+c_i \]

可知 \(k\) 单调增,目标是让 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL a[N], b[N], c[N];
LL dp[N];
inline LL X(int i) { return b[i]; }
inline LL Y(int i) { return dp[i] + c[i]; }
inline double slope(int i, int j) {
    double dy = Y(i) - Y(j), dx = X(i) - X(j);
    return dy / dx;
}
deque<int> q;
int main()
{
    //read
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; ++i) cin >> b[i];
    //init
    for (int i = 1; i <= n; ++i) c[i] = i * b[i];
    for (int i = 1; i <= n; ++i)
        b[i] += b[i - 1], c[i] += c[i - 1];
    //dp
    q.push_back(0);
    int tot = 1;
    for (int i = 1; i <= n; i++) {
        //delete
        while (tot > 1 && slope(q[0], q[1]) < i)
            q.pop_front(), tot--;
        //dp
        dp[i] = a[i] + dp[q[0]] - c[i] + c[q[0]] + i * (b[i] - b[q[0]]);
        //update
        while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
            q.pop_back(), tot--;
        //insert
        q.push_back(i), tot++;
    }
    cout << dp[n] << endl;
    return 0;
}

L题 防御准备

本题就是上题中 \(b_i=1\) 的子情况,直接改一下就可以了。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL a[N], c[N];
LL dp[N];
inline LL X(int i) { return i; }
inline LL Y(int i) { return dp[i] + c[i]; }
inline double slope(int i, int j) {
    double dy = Y(i) - Y(j), dx = X(i) - X(j);
    return dy / dx;
}
deque<int> q;
int main()
{
    //read
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    //init
    for (int i = 1; i <= n; ++i) c[i] = i;
    for (int i = 1; i <= n; ++i)
        c[i] += c[i - 1];
    //dp
    q.push_back(0);
    int tot = 1;
    for (int i = 1; i <= n; i++) {
        //delete
        while (tot > 1 && slope(q[0], q[1]) < i)
            q.pop_front(), tot--;
        //dp
        dp[i] = a[i] + dp[q[0]] - c[i] + c[q[0]] + 1LL * i * (i - q[0]);
        //update
        while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
            q.pop_back(), tot--;
        //insert
        q.push_back(i), tot++;
    }
    cout << dp[n] << endl;
    return 0;
}

M题 仓库建设

不难推出本题的普通方程:

\[dp_i=c_i+\max\limits_{0\leq j<i}\{dp_j+\sum\limits_{k=j+1}^i(x_i-x_k)p_k\} \]

我们维护一下 \(a_i=x_ip_i,b_i=p_i\),随后对 \(\{a_n\},\{b_n\}\) 维护一下前缀和,那么方程就变为了

\[dp_i=c_i+\max\limits_{0\leq j<i}\{dp_j+x_i(b_i-b_j)-(a_i-a_j)\} \]

移项转换,得

\[dp_i=c_i+dp_j+x_ib_i-x_ib_j+a_j-a_i \\ dp_j+a_j=x_ib_j+dp_i-c_i-x_ib_i+a_i \\ \begin{cases} y=dp_j+a_j\\k=x_i\\b=dp_i-c_i-x_ib_i+a_i \end{cases} \]

可知 \(k\) 单调增,目标是让 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。

这题在洛谷上面的 subtask(Hack数据)有点怪,我不好说了:

  1. \(p_i=0\) 恒成立,那么说明没有物资,也就不需要建立仓库
  2. 有时候末尾几处并没有物资,也就是说并非一定得在位置 n 处建立仓库。我们求出最后一个有物资的位置是 \(t\),那么答案就是 \(\min\limits_{t\leq i\leq n}dp_i\)。(为啥不是 \(dp_t\)?因为有时候后面建立仓库的价格特别小,可以抵消路途的路费)
  3. 这题算斜率的时候需要特意注意一下 \(dx=0\) 的情况,此时需要返回无穷大/无穷小
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL x[N], p[N], c[N];
LL a[N], b[N];
LL dp[N];
inline LL X(int i) { return b[i]; }
inline LL Y(int i) { return dp[i] + a[i]; }
inline double slope(int i, int j) {
    if (X(i) == X(j)) return 1e20;
    double dy = Y(i) - Y(j), dx = X(i) - X(j);
    return dy / dx;
}
deque<int> q;
int main()
{
    //read
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> x[i] >> p[i] >> c[i];
    //init
    for (int i = 1; i <= n; ++i) a[i] = x[i] * p[i];
    for (int i = 1; i <= n; ++i) b[i] = p[i];
    for (int i = 1; i <= n; ++i)
        a[i] += a[i - 1], b[i] += b[i - 1];
    //dp
    q.push_back(0);
    int tot = 1;
    for (int i = 1; i <= n; i++) {
        //delete
        while (tot > 1 && slope(q[0], q[1]) < x[i])
            q.pop_front(), tot--;
        //dp
        dp[i] = c[i] + dp[q[0]] + x[i] * (b[i] - b[q[0]]) - (a[i] - a[q[0]]);
        //update
        while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
            q.pop_back(), tot--;
        //insert
        q.push_back(i), tot++;
    }
    int t = 0;
    for (int i = n; i >= 1; i--)
        if (p[i]) { t = i; break; }
    LL ans = 1e18;
    for (int i = t; i <= n; ++i)
        ans = min(ans, dp[i]);
    cout << ans << endl;
    return 0;
}

N题 特别行动队

先预处理一下前缀和,\(s_i=\sum\limits_{k=1}^ix_i\)

本题的朴素动态规划转移方程为:

\[dp_i=\max\limits_{0\leq j<i}\{dp_j+a(s_i-s_j)^2+b(s_i-s_j)+c\} \]

移项更换,得

\[dp_i=dp_j+as_i^2+as_j^2-2as_is_j+bs_i-bs_j+c \\ bs_j-dp_j-as_j^2=-2as_is_j+c+as_i^2+bs_i-dp_i \\ \begin{cases} y=bs_j-dp_j-as_j^2 \\ k=-2as_i \\ b=as_i^2+b_i+c-dp_i \end{cases} \]

可知 \(k\) 单调增,目标是使得 \(b\) 最小化(即 \(dp_i\) 最大化),那么用单调队列维护一个下凸壳即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL a, b, c, s[N], dp[N];
inline LL X(int i) {
    return s[i];
}
inline LL Y(int i) {
    return -dp[i] - a * s[i] * s[i] + b * s[i];
}
inline double slope(int i, int j) {
    double dy = Y(i) - Y(j), dx = X(i) - X(j);
    return dy / dx;
}
deque<int> q;
int main()
{
    cin >> n >> a >> b >> c;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        s[i] += s[i - 1];
    }
    q.push_back(0);
    int tot = 1;
    for (int i = 1; i <= n; i++) {
        //delete
        while (tot > 1 && slope(q[0], q[1]) < -2 * a * s[i])
            q.pop_front(), tot--;
        //dp
        dp[i] = dp[q[0]] + a * (s[i] - s[q[0]]) * (s[i] - s[q[0]]) + b * (s[i] - s[q[0]]) + c;
        //update
        while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
            q.pop_back(), tot--;
        //insert
        q.push_back(i), tot++;
    }
    cout << dp[n] << endl;
    return 0;
}

杂项

C题 栅栏(搜索,剪枝)

给定 \(m\) 块木棒,第 \(i\) 根的长度为 \(a_i\)

现在我们想要得到 \(n\) 根木棒,且第 \(i\) 根的长度为 \(b_i\),我们可以将自己有的木棒裁剪一下(例如一根长度 10 的木棒,可以剪成一根 8 和 一根 2)以尽量达成目标。

问,我们至多可以得到多少根目标木棒?

\(m\leq 50,n\leq 10^3,1\leq a_i,b_i\leq 2^{15}\)

这个最大木板数量可以二分,而选择的时候也一定是优先选那些短的目标木板。那么,问题就变成了:能否用当前这些木板裁剪出我们需要的木板?

讲真,好长时间不写爆搜了(高三不谈,大学两年ACM因为必须写正解,所以也不咋去骗分),导致我一开始没写得出这个基础 DFS,好一会才想起来咋枚举:从前到后,依次检查第 \(i\) 块目标木棒选定了哪一个已有木棒,减去一下后继续搜,直到所有都达成。

bool dfs(int d) {
    if (d == 0) return true;
    for (int i = 1, f; i <= m; ++i)
        if (a[i] >= b[d]) {
            a[i] -= b[d], f = dfs(d - 1), a[i] += b[d];
            if (f) return true;
        }
    return false;
}

但毋庸置疑,这个爆搜不用刻意卡都会 T,所以我们看看咋优化:

  • 可行性剪枝

    如果当前剩下的木棒长度小于目标木棒的剩余长度,直接返回 false

  • 搜索树优化

    1. 先处理大的,后处理小的(如果成不了,让他在搜索树的前几层就断掉,而不是一直拖到后面)
    2. 如果两个相邻目标木棒长度相同,假设前者选择了现有木棒 \(i\),那么后者枚举的时候也从 \(i\) 开始,而非从 1 开始(两种行为本质上是等价的,所以可以强制要求下一次枚举的时候位置大于上一次)
#include<bits/stdc++.h>
using namespace std;
const int M = 60, N = 1010;
int m, n, a[M], b[N], s[N];
bool dfs(int d, int left, int pos) {
    if (s[d] > left) return false;
    if (d == 0) return true;
    for (int i = pos; i <= m; ++i)
        if (a[i] >= b[d]) {
            a[i] -= b[d];
            int f = dfs(d - 1, left - b[d], b[d] == b[d - 1] ? i : 1);
            a[i] += b[d];
            if (f) return true;
        }
    return false;
}
int main()
{
    int sum = 0;
    //read
    cin >> m;
    for (int i = 1; i <= m; ++i)
        cin >> a[i], sum += a[i];
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> b[i];
    sort(b + 1, b + n + 1);
    for (int i = 1; i <= n; ++i)
        s[i] = s[i - 1] + b[i];
    //Binary Search
    int l = 0, r = n;
    while (l < r) {
        int mid = (l + r + 1) >> 1;
        if (dfs(mid, sum, 1)) l = mid;
        else r = mid - 1;
    }
    cout << l << endl;
    return 0;
}

D题 牛跑步(K短路,A*)

给定一张 \(n\)\(m\) 边的边带权有向图,求出起点到终点的 最短路,次短路,......,k 短路的值。

\(1\leq n\leq 10^3,1\leq m\leq 10^4,k\leq 100,1\leq w_i\leq 10^6\)

K短路板子题,我们先求出最短路径,然后用这个作为启发函数,从而来跑A*。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
//test2
const int N = 1010, M = 10010;
int n, m, k;
struct Graph {
    int tot = 0, ver[M], edge[M], Head[N], Next[M];
    void addEdge(int x, int y, int z) {
        ver[++tot] = y, edge[tot] = z;
        Next[tot] = Head[x], Head[x] = tot;
    }
} G1, G2;
//First-Dijkstra
struct Node1 {
    int x; LL d;
    bool operator < (const Node1 &rhs) const {
        return d > rhs.d;
    }
};
LL g[N];
void FirstDijkstra() {
    priority_queue<Node1> q;
    memset(g, 0x3f, sizeof(g));
    g[1] = 0, q.push((Node1){1, 0});
    while (!q.empty()) {
        Node1 now = q.top(); q.pop();
        int x = now.x;
        if (g[x] != now.d) continue;
        for (int i = G2.Head[x]; i; i = G2.Next[i]) {
            int y = G2.ver[i], z = G2.edge[i];
            if (g[y] > g[x] + z) {
                g[y] = g[x] + z;
                q.push((Node1){y, g[y]});
            }
        }
    }
}
//Astar
struct Node2 {
    int x; LL d;
    bool operator < (const Node2 &rhs) const {
        return d + g[x] > rhs.d + g[rhs.x];
    }
};
void Astar() {
    priority_queue<Node2> q;
    q.push((Node2){n, 0});
    int cnt = 0;
    while (!q.empty()) {
        Node2 now = q.top(); q.pop();
        int x = now.x; LL d = now.d;
        if (x == 1) {
            printf("%lld\n", d);
            if (++cnt == k) break;
        }
        for (int i = G1.Head[x]; i; i = G1.Next[i]) {
            int y = G1.ver[i], z = G1.edge[i];
            q.push((Node2){y, d + z});
        }
    }
    while (cnt++ < k) puts("-1");
}
int main()
{
    cin >> n >> m >> k;
    for (int i = 0; i < m; ++i) {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        G1.addEdge(x, y, z), G2.addEdge(y, x, z);
    }
    FirstDijkstra();
    Astar();
    return 0;
}
posted @ 2022-08-07 20:00  cyhforlight  阅读(21)  评论(0编辑  收藏  举报