算法模板(4):动态规划(4) 做题积累(2)

动态规划

9. 单调队列优化DP

1. 1088. 旅行问题

John 打算驾驶一辆汽车周游一个环形公路。

公路上总共有 n 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。

John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。

在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。

任务:判断以每个车站为起点能否按条件成功周游一周。

  • 做法:把环形序列扩展一倍成一条链,设距离是 d i d_i di,加油站的油量是 o i o_i oi,打一个 o i − d i o_i - d_i oidi 的前缀和,从第 k k k 个点出发可以到达,等价于 ∀ i ∈ [ k , k + n − 1 ] , s [ i ] − s [ k − 1 ] ≥ 0 \forall i \in [k, k + n - 1],s[i]-s[k - 1] \ge 0 i[k,k+n1],s[i]s[k1]0,即可以找到长度为 n n n 的窗口的最小值,然后减去 s [ k − 1 ] s[k - 1] s[k1] 看看是否大于等于 0.
  • 细节巨多.
#include
using namespace std;
const int N = 2000010;
typedef long long ll;
ll o[N], d[N], s[N];
int q[N], n;
int st[N];

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d%d", &o[i], &d[i]);
    for(int i = 1; i <= n; i++)
    {
        s[i] = s[i + n] = o[i] - d[i];
    }
    for(int i = 1; i <= 2 * n; i++) s[i] += s[i - 1];

    int hh = 0, tt = -1;
    
    //顺时针
    for(int i = 2 * n; i; i--)
    {
        if(q[hh] >= n + i) hh++;
        while(hh <= tt && s[q[hh]] >= s[i]) tt--;
        q[++tt] = i;
        if(i <= n)
        {
            if(s[q[hh]] - s[i - 1] >= 0) st[i] = true;
        }
    }
    
    //逆时针,整个前缀和序列会发生变化
    
    hh = 0, tt = -1;
    d[0] = d[n];
    for(int i = 1; i <= n; i++) s[i] = s[i + n]= o[i] - d[i - 1];
    for(int i = 2 * n; i; i--) s[i] += s[i + 1];
    
    for(int i = 1; i <= 2 * n; i++)
    {
        if(q[hh] <= i - n) hh++;
        while(hh <= tt && s[q[hh]] >= s[i]) tt--;
        q[++tt] = i;
        if(i > n)
        {
            //注意这里是 st[i - n] 
            if(s[q[hh]] - s[i + 1] >= 0) st[i - n] = true;
        }
    }
    
    for(int i = 1; i <= n; i++)
    {
        printf("%s\n", st[i] ? "TAK" : "NIE");
    }
    return 0;
}

2. 1090. 绿色通道

  • 高二数学《绿色通道》总共有 n n n 道题目要抄,编号 1 , 2 , … , n 1,2,…,n 1,2,,n,抄第 i i i 题要花 a i a_i ai 分钟。小 Y 决定只用不超过 t t t 分钟抄这个,因此必然有空着的题。连续空着的题目(称为空题段)越多,老师越生气。小 Y 想让最长的空题段长度尽可能地小,请输出这个最小值.
  • 二分一下,就和 烽火传递 思路一样了.
#include
using namespace std;
const int N = 50010;
int q[N], w[N], f[N];
int n, t;

bool check(int m)
{
    int hh = 0, tt = -1;
    q[++tt] = 0;
    for(int i = 1; i <= n; i++)
    {
        if(q[hh] < i - m - 1) hh++;
        f[i] = f[q[hh]] + w[i];
        while(hh <= tt && f[q[tt]] >= f[i]) tt--;
        q[++tt] = i;
    }
    
    for(int i = n - m; i <= n; i++)
    {
        if(f[i] <= t) return true;
    }
    return false;
}

int main()
{
    scanf("%d%d", &n, &t);
    for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
    int l = 0, r = n;
    while(l < r)
    {
        int mid = (l + r) / 2;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }
    printf("%d\n", l);
    return 0;
}

3. 1087. 修剪草坪

FJ 有 N N N 只排成一排的奶牛,编号为 1 1 1 N N N。每只奶牛的效率是不同的,奶牛 i i i 的效率为 E i E_i Ei。编号相邻的奶牛们很熟悉,如果 FJ 安排超过 K K K 只编号连续的奶牛,那么这些奶牛就会罢工去开派对。因此,现在 FJ 需要你的帮助,找到最合理的安排方案并计算 FJ 可以得到的最大效率。注意,方案需满足不能包含超过 K K K 只编号连续的奶牛。

这个题有一个很简单的做法,就是类似于 烽火传递 那个题,不过我们这次是选出来不选的牛,让这些牛的效率之和最小。可以转化为在长度为 K + 1 K+1 K+1 的连续子序列中至少选择一个数字问最小值 r e s res res,所有权值之和是 s u m sum sum,答案就是 s u m − r e s . sum - res. sumres.

f ( i ) f(i) f(i):在前 i i i 头牛里面选,方案最大值。那么不选第 i i i 头牛的话就是 f ( i ) = f ( i − 1 ) f(i) = f(i - 1) f(i)=f(i1),当然也可以选择从 i − j + 1 ∼ i i - j + 1 \sim i ij+1i 头牛,且不选第 i − j i - j ij 头牛。就是 f ( i − j − 1 ) + s [ i ] − s [ i − j ] f(i - j -1) + s[i] - s[i - j] f(ij1)+s[i]s[ij],记 f ( i − j − 1 ) − s [ i − j ] = g ( i − j ) f(i - j - 1) - s[i - j] = g(i - j) f(ij1)s[ij]=g(ij),即 g ( i ) = f ( i − 1 ) − s [ i ] g(i) = f(i - 1) - s[i] g(i)=f(i1)s[i]. 因此 f ( i ) = max ⁡ { f ( i − 1 ) , g ( i − j ) + s [ i ] } f(i) = \max\{f(i -1), g(i - j) + s[i]\} f(i)=max{f(i1),g(ij)+s[i]}. 因此我们需要找到 max ⁡ \max\limits_{} max

算法模板(4):动态规划(4) 做题积累(2)_第1张图片

#include
using namespace std;
const int N = 100010;
typedef long long ll;
ll f[N], g[N], q[N], s[N];
int n, k;
int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 1; i <= n; i++)
    {
        scanf("%lld", &s[i]);
        s[i] += s[i - 1];
    }

    int hh = 0, tt = -1;
    q[++tt] = 0;
    for(int i = 1; i <= n; i++)
    {
        if(q[hh] < i - k) hh++;
        f[i] = max(f[i - 1], g[q[hh]] + s[i]);
        g[i] = f[i - 1] - s[i];
        while(hh <= tt && g[q[tt]] <= g[i]) tt--;
        q[++tt] = i;
    }

    printf("%lld\n", f[n]);
    return 0;
}

4. 1091. 理想的正方形

  • 题意:有一个 a × b a\times b a×b 的整数组成的矩阵,现请你从中找出一个 n × n n\times n n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
  • 二维单调队列,求方框内的最大值和最小值.

保存一个二维单调队列 d e q u e < i n t > c o l [ M ] deque col[M] deque<int>col[M],记录第 j j j 列的滑动窗口最大值;以及当前行的单调队列 d e q u e < i n t > r o w dequerow deque<int>row,记录方框内 c o l [ j − k + 1 : j ] . f r o n t ( ) col[j - k + 1:j].front() col[jk+1:j].front() 的最大值。每次先用 w [ i , j ] w[i,j] w[i,j] 更新第 j j j 列的状态(即 c o l [ j ] . b a c k ( ) col[j].back() col[j].back()),再用 c o l [ j ] . f r o n t ( ) col[j].front() col[j].front() 更新 r o w row row 的状态.

这道题卡时间了,然后我把 O1 O2 O3 优化全开了,然后就过了.

#include
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#define x first
#define y second

using namespace std;
const int N = 1010;
typedef pair<int, int> P;
deque<int> col_max[N], col_min[N];
deque<P> row_max, row_min;
int n, m, k;
int w[N][N];
int ans = 0x3f3f3f3f;
int main()
{
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            scanf("%d", &w[i][j]);
        }
    }
    for(int i = 1; i <= n; i++)
    {
        row_max.clear(), row_min.clear();
        for(int j = 1; j <= m; j++)
        {
            if(col_max[j].size() && col_max[j].front() < i - k + 1) col_max[j].pop_front();
            if(col_min[j].size() && col_min[j].front() < i - k + 1) col_min[j].pop_front();
            if(row_max.size() && row_max.front().y < j - k + 1) row_max.pop_front();
            if(row_min.size() && row_min.front().y < j - k + 1) row_min.pop_front();

            while(col_max[j].size() && w[col_max[j].back()][j] <= w[i][j]) col_max[j].pop_back();
            col_max[j].push_back(i);
            while(col_min[j].size() && w[col_min[j].back()][j] >= w[i][j]) col_min[j].pop_back();
            col_min[j].push_back(i);

            while(row_max.size() && row_max.back().x <= w[col_max[j].front()][j]) row_max.pop_back();
            row_max.push_back({w[col_max[j].front()][j], j});
            while(row_min.size() && row_min.back().x >= w[col_min[j].front()][j]) row_min.pop_back();
            row_min.push_back({w[col_min[j].front()][j], j});

            if(i >= k && j >= k)
            {
                ans = min(ans, row_max.front().x - row_min.front().x);
            }
        }
    }
    printf("%d\n", ans);
}

10. 斜率优化DP(凸包优化)

1.1 301. 任务安排2

1.2 302. 任务安排3

N N N 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。机器会把这 N N N 个任务分成若干批,每一批包含连续的若干个任务。从时刻 0 0 0 开始,任务被分批加工,执行第 i i i 个任务所需的时间是 T i T_i Ti。另外,在每批任务开始前,机器需要 S S S 的启动时间,故执行一批任务所需的时间是启动时间 S S S 加上每个任务所需时间之和。一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。也就是说,同一批任务将在同一时刻完成。每个任务的费用是它的完成时刻乘以一个费用系数 C i C_i Ci。请为机器规划一个分组方案,使得总费用最小。

情况一:

  • 1 ≤ n ≤ 5000 , 0 ≤ S ≤ 50 , 1 ≤ T i , C i ≤ 100 1 \le n \le 5000,0\le S \le 50,1 \le T_i,C_i \le 100 1n5000,0S50,1Ti,Ci100. 用 O ( n 2 ) O(n^2) O(n2) 来写

f ( i ) f(i) f(i) 为分配任务 1 ∼ i 1 \sim i 1i 的最小花费。假设上一批最后一个任务编号是 j j j. 那么,新分配一个任务的对答案多出来的启动时间的花费是 S ∗ ( s u m C n − s u m C j ) S * (sumC_n - sumC_j) S(sumCnsumCj) ,任务本身的花费是 s u m T i ∗ ( s u m C i − s u m C j ) sumT_i*(sumC_i - sumC_j) sumTi(sumCisumCj). 即 f ( i ) = min ⁡ 0 ≤ j < i { f ( j ) + s u m T i ∗ ( s u m C i − s u m C j ) + S ∗ ( s u m C n − s u m C j ) } f(i) = \min\limits_{0 \le j < i} \{ f(j) + sumT_i*(sumC_i - sumC_j) + S * (sumC_n - sumC_j)\} f(i)=0j<imin{f(j)+sumTi(sumCisumCj)+S(sumCnsumCj)}.

#include
using namespace std;
const int N = 5010;
int f[N], sumC[N], sumT[N];
int n, S;
int main()
{
    scanf("%d%d", &n, &S);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d%d", &sumT[i], &sumC[i]);
        sumT[i] += sumT[i - 1], sumC[i] += sumC[i - 1];
    }
    
    memset(f, 0x3f, sizeof f);
    f[0] = 0;
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j < i; j++)
        {
            f[i] = min(f[i], f[j] + sumT[i] * (sumC[i] - sumC[j]) + S * (sumC[n] - sumC[j])); 
        }
    }
    printf("%d\n", f[n]);
    return 0;
}

情况二

  • 1 ≤ n ≤ 3 ∗ 1 0 5 , 1 ≤ T i , C i ≤ 512 , 0 ≤ S ≤ 512 1 \le n \le 3*10^5,1≤T_i,C_i≤512,0≤S≤512 1n3105,1Ti,Ci512,0S512.
  • 由于和 i i i 相关的可以看作定值,和 j j j 相关的可以看作变量。因此原等式可以这样做变化: f ( j ) = ( s u m T i + S ) ∗ s u m C j + f ( i ) − s u m T i ∗ s u m C i − S ∗ s u m C n f(j) = (sumT_i + S) * sumC_j +f(i) - sumT_i * sumC_i - S * sumC_n f(j)=(sumTi+S)sumCj+f(i)sumTisumCiSsumCn.

可以看作直线 y = k x + b y = kx + b y=kx+b y y y f ( j ) f(j) f(j),斜率是 s u m T i + S sumT_i+S sumTi+S x x x s u m C j sumC_j sumCj,截距 b b b 可以看作 f ( i ) − s u m T i ∗ s u m C i − S ∗ s u m C n f(i) - sumT_i * sumC_i - S * sumC_n f(i)sumTisumCiSsumCn . 而 f ( j ) f(j) f(j) s u m C j sumC_j sumCj 是之前就计算出来的,目标是让 f ( i ) f(i) f(i) 最小,就是让截距最小. 我们发现,如果把 ( s u m C j , f ( j ) ) (sumC_j,f(j)) (sumCj,f(j)) 画在坐标系中,那么可以和答案有关的点一定是在凸包的边界上。这道题的斜率都是正数.

此题特点是斜率单调递增,横坐标也单调递增

在查询的时候,可以将队头小于当前斜率的点全部删掉;在插入的时候,将队尾所有不在凸包上的点全部删掉. 当然对于这个题横坐标 s u m C i sumC_i sumCi 本身就是单调递增的.

因此查询的点一定是一直往右走,因此查询的时候遇到斜率小与 s u m C i sumC_i sumCi 的就可以删掉了(但是要注意与上一个点的相对斜率不一定会比上一个相对斜率大).

算法模板(4):动态规划(4) 做题积累(2)_第2张图片

#include
using namespace std;
const int N = 300010;
typedef long long ll;

ll f[N], C[N], T[N], q[N], s;
int n;
int main()
{
    scanf("%d%lld", &n, &s);

    for(int i = 1; i <= n; i++)
    {
        scanf("%lld%lld", &T[i], &C[i]);
        T[i] += T[i - 1], C[i] += C[i - 1];
    }

    int hh = 0, tt = -1;
    q[++tt] = 0;

    for(int i = 1; i <= n; i++)
    {
        while(hh < tt && f[q[hh + 1]] - f[q[hh]] <= (T[i] + s) * (C[q[hh + 1]] - C[q[hh]])) hh++;
        int j = q[hh];
        f[i] = f[j] + T[i] * C[i] + s * C[n] - (T[i] + s) * C[j];
        while(hh < tt && (f[q[tt]] - f[q[tt - 1]]) * (C[i] - C[q[tt]]) >= (f[i] - f[q[tt]]) * (C[q[tt]] - C[q[tt - 1]])) tt--;
        q[++tt] = i;
    }
    printf("%lld\n", f[n]);

    return 0;
}

情况三

  • 1 ≤ n ≤ 3 ∗ 1 0 5 , 0 ≤ S , C i ≤ 512 , − 512 ≤ T i ≤ 512. 1 \le n \le 3 * 10 ^5, 0 \le S,C_i \le 512,-512 \le T_i \le 512. 1n3105,0S,Ci512,512Ti512.

  • 此题特点是斜率可能不单调,但横坐标 s u m C i sumC_i sumCi 仍然是单调递增的.

  • 处理方法是:在查询的时候,只能二分来找;在插入的时候,把不在凸包上的点全部删掉.

补充一句,如果横坐标不单调递增,那么需要有增添操作,那么需要用平衡树维护.

#include
using namespace std;
const int N = 300010;
typedef long long ll;
ll s, C[N], T[N], f[N];
int n, q[N];
int main()
{
    scanf("%d%lld", &n, &s);
    for(int i = 1; i <= n; i++)
    {
        scanf("%lld%lld", &T[i], &C[i]);
        T[i] += T[i - 1], C[i] += C[i - 1];
    }

    int hh = 0, tt = -1;
    q[++tt] = 0;
    for(int i = 1; i <= n; i++)
    {
        int l = hh, r = tt;
        //考虑清楚二分的边界.
        while(l < r)
        {
            int mid = (l + r) / 2;
            if((f[q[mid + 1]] - f[q[mid]]) > (__int128)(T[i] + s) * (C[q[mid + 1]] - C[q[mid]])) r = mid;
            else l = mid + 1;
        }

        int j = q[l];
        f[i] = f[j] + (__int128)T[i] * C[i] + (__int128)s * C[n] - (__int128)(T[i] + s) * C[j];
        while(hh < tt && (__int128)(f[q[tt]] - f[q[tt - 1]]) * (C[i] - C[q[tt]]) >= (__int128)(f[i] - f[q[tt]]) * (C[q[tt]] - C[q[tt - 1]])) tt--;
        q[++tt] = i;
    }
    printf("%lld\n", f[n]);
    return 0;
}

303. 运输小猫

  • S S S 是农场主,他养了 M M M 只猫,雇了 P P P 位饲养员。农场中有一条笔直的路,路边有 N N N 座山,从 1 1 1 N N N 编号。第 i i i 座山与第 i − 1 i−1 i1 座山之间的距离为 D i D_i Di。饲养员都住在 1 1 1 号山。第 i i i 只猫去 H i H_i Hi 号山玩,玩到时刻 T i T_i Ti 停止,然后在原地等饲养员来接。饲养员在路上行走需要时间,速度为 1 米/单位时间。你的任务是规划每个饲养员从 1 号山出发的时间,使得所有猫等待时间的总和尽量小。饲养员出发的时间可以为负。

  • d d d 是前缀和, t t t 是结束时间,那么,对于第 i i i 只猫,只有饲养员在 t i − d i t_i - d_i tidi 之后出发才能接到这只猫。因此我们设 a i = t i − d i a_i = t_i - d_i ai=tidi,那么按照 a a a 排序,一位饲养员接一段连续子序列的小猫,那么相当于把这个子序列分为不超过 P P P 份,每位饲养员恰好接到当前子序列的最后一只小猫. 假设饲养员是 s i s_i si 出发的,恰好接到小猫 i i i,那么小猫等待时间就是 s i − a i . s_i - a_i. siai.

  • f ( j , i ) f(j, i) f(j,i) 为第 j j j 只饲养员接的最后一只小猫是 i i i. 上一个饲养员接的小猫是 k k k,那么 f ( j , i ) = min ⁡ { f ( j − 1 , k ) + a i ∗ ( i − k ) − ( s i − s k ) } f(j,i) = \min \{f(j - 1, k) + a_i * (i - k) - (s_i - s_k)\} f(j,i)=min{f(j1,k)+ai(ik)(sisk)}.

f ( j − 1 , k ) + s k = a i ∗ k + f ( j , i ) − a i ∗ i + s i f(j - 1,k) + s_k = a_i * k + f(j, i) - a_i * i + s_i f(j1,k)+sk=aik+f(j,i)aii+si.

#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010, M = 100010, P = 110;

int n, m, p;
LL d[N], t[N], a[N], s[N];
LL f[P][M];
int q[M];

LL get_y(int k, int j)
{
    return f[j - 1][k] + s[k];
}

int main()
{
    scanf("%d%d%d", &n, &m, &p);

    for (int i = 2; i <= n; i ++ )
    {
        scanf("%lld", &d[i]);
        d[i] += d[i - 1];
    }

    for (int i = 1; i <= m; i ++ )
    {
        int h;
        scanf("%d%lld", &h, &t[i]);
        a[i] = t[i] - d[h];
    }

    sort(a + 1, a + m + 1);

    for (int i = 1; i <= m; i ++ ) s[i] = s[i - 1] + a[i];

    memset(f, 0x3f, sizeof f);
    for (int i = 0; i <= p; i ++ ) f[i][0] = 0;

    for (int j = 1; j <= p; j ++ )
    {
        int hh = 0, tt = 0;
        q[0] = 0;

        for (int i = 1; i <= m; i ++ )
        {
            while (hh < tt && (get_y(q[hh + 1], j) - get_y(q[hh], j)) <= a[i] * (q[hh + 1] - q[hh])) hh ++ ;
            int k = q[hh];
            f[j][i] = f[j - 1][k] - a[i] * k + s[k] + a[i] * i - s[i];
            while (hh < tt && (get_y(q[tt], j) - get_y(q[tt - 1], j)) * (i - q[tt]) >=
                (get_y(i, j) - get_y(q[tt], j)) * (q[tt] - q[tt - 1])) tt -- ;
            q[ ++ tt] = i;
        }
    }

    printf("%lld\n", f[p][m]);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/128992/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

11. 基环树DP

359. 创世纪

12. 四边形不等式DP

13. 插头DP

14. 环形与后效性处理

15. 倍增优化 DP

16. 数据结构优化 DP

你可能感兴趣的:(算法模板,算法,动态规划,图论)