【笔记】郑州大学ACM实验室寒假新生培训 : 动态规划

》》b站视频链接《《

》》b站视频链接《《

OP

个人认为,DP是一种通过保证每步下所有情况的最优解,从而达到总体的最优解的过程;

背包问题

01背包

01背包,即每种物品仅有使用与不使用两种状态;

问题描述

有 n 种物品和容量为 V 的背包,每种物品只能用一次;
第 i 件物品的体积是 v[ i ] ,价值是 w[ i ] ;
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大时的最大价值;

解法

f ( i , j ) f(i,j) f(i,j) 为仅考虑前 i 个物品的前提下,使用 j 容量能达到的最大价值;
则显然, f ( 0 , j ) = 0 f(0,j)=0 f(0,j)=0
对于 f ( i , j ) f(i,j) f(i,j) ,显然其只可能由 f ( i − 1 , j − v [ i ] ) f(i-1,j-v[i]) f(i1,jv[i]) 有关(即取第 i 件商品,共使用 j 份体积的情况,是由使用前 i - 1 件物品,共使用 j - v[ i ] 份体积的情况转换来的);

在已知 f ( i − 1 , x ) ( x ∈ [ 0 , V ] ) f(i-1,x)(x\in[0,V]) f(i1,x)(x[0,V]) 时,那么对于第 i 件物品和 j 份体积,其有两种情况:
①. 选择第 i 件物品总价值最大,此时 f ( i , j ) = f ( i − 1 , j − v [ i ] ) + w [ i ] f(i,j)=f(i-1,j-v[i])+w[i] f(i,j)=f(i1,jv[i])+w[i]
②. 不选择第 i 件物品总价值最大(即选择第 i 件物品 j 份体积下带来的价值不如前 i - 1 件物品 j 份体积下带来的价值),此时 f ( i , j ) = f ( i − 1 , j ) f(i,j)=f(i-1,j) f(i,j)=f(i1,j)

综上所述有状态转移方程: f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − v [ i ] ) + w [ i ] ) f(i,j)=max(f(i-1,j),f(i-1,j-v[i])+w[i]) f(i,j)=max(f(i1,j),f(i1,jv[i])+w[i])

由此,我们从 0 到 n 递推 i , f ( n , V ) f(n,V) f(n,V) 即为我们所求的答案;

在整个过程中,对于一个 f ( i , j ) f(i,j) f(i,j) ,我们不需要考虑第 i 个物品究竟有没有被选择,只需要考虑 f ( i , j ) f(i,j) f(i,j) 的值是多少;

在实际操作中,我们使用二维数组 f [ i ] [ j ] f[i][j] f[i][j] 来存储 f ( i , j ) f(i,j) f(i,j) ,同时要避免由 j − v [ i ] j-v[i] jv[i] 带来的数组越界;

完全背包

问题描述

有 n 种物品和容量为 V 的背包,每种物品有无限件可用
第 i 件物品的体积是 v[ i ] ,价值是 w[ i ] ;
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大时的最大价值;

解法

与01背包问题类似,完全背包问题有状态转移方程: f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i , j − v [ i ] ) + w [ i ] ) f(i,j)=max(f(i-1,j),f(i,j-v[i])+w[i]) f(i,j)=max(f(i1,j),f(i,jv[i])+w[i])

区别仅在 f ( x , j − v [ i ] ) + w [ i ] f(x,j-v[i])+w[i] f(x,jv[i])+w[i] 的 x 取 i 还是 i - 1 ;

在这个问题中,从 j 小到大计算 f ( i , j ) f(i,j) f(i,j) 的过程中,如果后项大于前项,即意味着此物品被重复使用了;

多重背包

问题描述

有 n 种物品和容量为 V 的背包,每种物品有s[i]件可用
第 i 件物品的体积是 v[ i ] ,价值是 w[ i ] ;
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大时的最大价值;

解法1

对于每一件物品 i ,将它的 s[ i ] 件拆开考虑,则变成了总共有 ∑ i = 1 n s [ i ] \sum_{i=1}^ns[i] i=1ns[i] 件物品的01背包;

解法2(二进制拆分)

解法1所需的时间复杂度过大,所以我们可以对 s[ i ] 进行二进制拆分;

拆分的需求是把 s [ i ] s[ i ] s[i] 拆分成几部分,使得这若干部分的和可以且仅可以表示区间 [ 0 , s [ i ] ] [ 0 , s[ i ] ] [0,s[i]] 间的任意一个数;
我们将 s [ i ] s[ i ] s[i] 拆分成:
2 0 , 2 1 , 2 2 , . . . , 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ − 1 , s [ i ] − ( 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ − 1 ) 2^0,2^1,2^2,...,2^{\lfloor log_2(s[i]+1)\rfloor-1},s[i]-(2^{\lfloor log_2(s[i]+1)\rfloor}-1) 20,21,22,...,2log2(s[i]+1)1,s[i](2log2(s[i]+1)1) ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 \lfloor log_2(s[i]+1)\rfloor+1 log2(s[i]+1)+1 个数;

其中,区间 [ 0 , 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ − 1 ] [0,2^{\lfloor log_2(s[i]+1)\rfloor}-1] [0,2log2(s[i]+1)1] 区间的任意一个整数可以仅由前 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ \lfloor log_2(s[i]+1)\rfloor log2(s[i]+1) 个数用二进制组合合成,
同时,将前区间的每个元素加上 s [ i ] − 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 s[i]-2^{\lfloor log_2(s[i]+1)\rfloor}+1 s[i]2log2(s[i]+1)+1 ,即区间 [ s [ i ] − 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 , s [ i ] ] [s[i]-2^{\lfloor log_2(s[i]+1)\rfloor}+1,s[i]] [s[i]2log2(s[i]+1)+1,s[i]] ,可由前面集合的某一项加上最后一个数得来;

由于

s [ i ] + 1 < 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 s[i]+1\lt 2^{\lfloor log_2(s[i]+1)\rfloor+1} s[i]+1<2log2(s[i]+1)+1

s [ i ] < 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 − 1 s[i]\lt2^{\lfloor log_2(s[i]+1)\rfloor+1}-1 s[i]<2log2(s[i]+1)+11
s [ i ] ⩽ 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 − 2 s[i]\leqslant2^{\lfloor log_2(s[i]+1)\rfloor+1}-2 s[i]2log2(s[i]+1)+12
s [ i ] − 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ + 1 ⩽ 2 ⌊ l o g 2 ( s [ i ] + 1 ) ⌋ − 1 s[i]-2^{\lfloor log_2(s[i]+1)\rfloor}+1\leqslant 2^{\lfloor log_2(s[i]+1)\rfloor}-1 s[i]2log2(s[i]+1)+12log2(s[i]+1)1

所以,两个集合的并集即为 [ 0 , s [ i ] ] [ 0 , s[ i ] ] [0,s[i]] ,此种拆分方式可以达到目的;

在实际操作中,拆分的过程不需要如此复杂,可由以下伪代码实现:

int two=1;
for(;two<=s;s-=two,two<<=1)
{
	operate(two);
}
operate(s);

空间优化

我们可以观察到,对每一件物品的处理过程中,只涉及到了 f ( i , j ) f(i,j) f(i,j) f ( i − 1 , j ) f(i-1,j) f(i1,j) ,所以只需要存储下这两组数据即可;

对于01背包,由于需要保证 j − v [ i ] j-v[i] jv[i] 这一项的数据是 i − 1 i-1 i1 时的,我们需要从后向前遍历 j ;
即:

for(int i=1;i<=n;i++)
	for(int j=V;j>=v[i];j--)
	{
		f[j]=max(f[j],f[j-v[i]]+w[i]);
	}

同理,对于完全背包, j − v [ i ] j-v[i] jv[i] 这一项的数据是 i i i 时的,我们从前向后遍历 j 即可;
即:

for(int i=1;i<=n;i++)
	for(int j=v[i];j<=V;j++)
	{
		f[j]=max(f[j],f[j-v[i]]+w[i]);
	}

状压DP

状压DP是用二级制位来描述状态的一种DP;

具体来说,可以通过类似二进制枚举的方式遍历到每一个状态,进而进行处理;


例1:HDUOJ.5418 Victor and World
题目链接

题目大意

有n座城和m条无向道路,给出每条道路的权值w和两端的城市,求问从1号城出发经过每个城市一次后回到一号城的回路中,总权值的最小值;

数据范围

n ⩽ 16 n\leqslant16 n16
w i ⩽ 100 w_i\leqslant100 wi100

我们可以在floyd算法求出多源最短路的情况下,进行以下DP策略;

定义状态 s,若 s 的第 i-1 位为 1 ,即表示在s所表示的状态中,i 号城已经被经过( s 的第 i-1 位为 1 等价为(s>>(i-1))&1=1);
定义dp方程 d p [ i ] [ s ] dp[i][s] dp[i][s] ,表示在状态 s 下,以 i 为最后一城的最小总权值(在dp执行的过程中, (s>>(i-1))&1=1 恒成立);
定义状态转移方程 d p [ j ] [ s ∣ ( 1 < < ( i − 1 ) ) ] = m i n ( d p [ j ] [ s ∣ ( 1 < < ( i − 1 ) ) ] , d p [ i ] [ s ] + d i s [ i ] [ j ] ) dp[j][s|(1<<(i-1))]=min(dp[j][s|(1<<(i-1))],dp[i][s]+dis[i][j]) dp[j][s(1<<(i1))]=min(dp[j][s(1<<(i1))],dp[i][s]+dis[i][j]) ,实际意义为从 i 城多走一段 dis[i][j] 到 j 城;

最后输出答案时,遍历 d p [ i ] [ ( 1 < < n ) − 1 ]   , i ∈ [ 1 , n ] dp[i][(1<<n)-1]\ ,i\in[1,n] dp[i][(1<<n)1] ,i[1,n] ,找出 ( d p [ i ] [ ( 1 < < n ) − 1 ] + d i s [ i ] [ 1 ] ) m i n (dp[i][(1<<n)-1]+dis[i][1])_{min} (dp[i][(1<<n)1]+dis[i][1])min 即可;

代码如下:

时间复杂度 O ( 2 n n 2 ) O(2^nn^2) O(2nn2)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 5;
int d[21][21]; //存图,无边的位置为INF,主对角线为0
int dp[21][66004];
int t, n, m;
void floyd()
{
    for (int i = 1; i <= n; i++)
        d[i][i] = 0;
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main()
{
    int a, b, w;
    cin >> t;
    while (t--)
    {
        scanf("%d%d", &n, &m);
        memset(d, 0x3f, sizeof d);
        memset(dp, 0x3f, sizeof dp);
        while (m--)
        {
            scanf("%d%d%d", &a, &b, &w);
            if (a == b)
                continue;
            d[a][b] = min(d[a][b], w), d[b][a] = min(d[b][a], w);
        }
        floyd();
        dp[1][1] = 0;
        for (int s = 1; s <= (1ll << n) - 1; s++)//遍历状态
        {
            for (int i = 1; i <= n; i++)
            {
                if ((s >> (i - 1)) & 1)//遍历可行末点
                {
                    for (int j = 1; j <= n; j++)//遍历下一点
                    {
                        dp[j][s | (1 << (j - 1))] = min(dp[j][s | (1 << (j - 1))], dp[i][s] + d[i][j]);
                    }
                }
            }
        }
        int ans = INF;
        for (int i = 1; i <= n; i++)//维护ans
        {
            ans = min(ans, dp[i][(1ll << n) - 1] + d[i][1]);
        }
        printf("%d\n", ans);
    }
}

例2:洛谷P1896 互不侵犯
题目链接

题目大意

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它周围的8个格子。

数据范围

( 1 < = N < = 9   , 0 < = K < = N ⋅ N ) ( 1 <=N <=9\ , 0 <= K <= N \cdotp N) (1<=N<=9 ,0<=K<=NN)

类似地,我们可以定义出一行的状态 s ,若 s 的第 i-1 位为 1 ,即表示在s所表示的状态中,第 i 位放有国王;
定义dp方程 d p [ i ] [ s ] [ c ] dp[i][s][c] dp[i][s][c] 为在第 i 行为状态 s 的情况下,前 i 行放入国王总数为 c 的方案数;
由此我们可以定义状态转移方程 d p [ i ] [ s ] [ c ] = ∑ d p [ i − 1 ] [ t ] [ c − b i t c o u n t ( s ) ]   , s   t dp[i][s][c]=\sum dp[i-1][t][c-bitcount(s)]\ ,s\ t dp[i][s][c]=dp[i1][t][cbitcount(s)] ,s t 不冲突;

对于状态 s ,不考虑行间干扰,一行中所有可行的 s 均满足 s & ( s < < 1 ) = 0 s\&(s<<1)=0 s&(s<<1)=0 ,可以通过这条性质进行预处理出所有可行 s ;
对于s,t不冲突,意味着满足

s & t = 0 s & ( t < < 1 ) = 0 ( s < < 1 ) & t = 0 s\&t=0\\s\&(t<<1)=0\\(s<<1)\&t=0 s&t=0s&(t<<1)=0(s<<1)&t=0

这三条性质;

由此,我们可以遍历行,上行状态,本行状态,棋子可行总数来更新;
输出答案时, a n s = ∑ s i 可 行 d p [ n ] [ s i ] [ k ] ans=\sum_{s_i可行}dp[n][s_i][k] ans=sidp[n][si][k]

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 5;
int bitcount(int i)//O(1)bitcount
{
    i = i - ((i >> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
    i = (i + (i >> 4)) & 0x0f0f0f0f;
    i = i + (i >> 8);
    i = i + (i >> 16);
    return i & 0x3f;
}
pair<int,int> stt[1000];
ll dp[11][1003][102];
int main()
{
    int n,m,c=0;
    cin>>n>>m;
    for(int i=0;i<=(1<<n)-1;i++)
    {
        if(!(i&(i<<1)))//预处理可行状态
        {
            stt[c++]={i,bitcount(i)};
        }
    }
    for(int i=0;i<c;i++)//第1行初始化
    {
        dp[1][stt[i].first][stt[i].second]=1;
    }
    for(int i=2;i<=n;i++)
    {
        for(int j=0;j<c;j++)//枚举上行
        {
            for(int k=0;k<c;k++)//枚举本行
            {
                if(stt[j].first&stt[k].first
                    ||(stt[j].first<<1)&stt[k].first
                    ||stt[j].first&(stt[k].first<<1))//冲突则continue
                    continue;
                for(int l=stt[k].second;l<=m;l++)dp[i][stt[k].first][l]+=dp[i-1][stt[j].first][l-stt[k].second];//枚举国王数可行值
            }
        }
    }
    ll ans=0;
    for(int i=0;i<c;i++)
    {
        ans+=dp[n][stt[i].first][m];
    }
    cout<<ans;
}

单调队列优化DP

对于一些在dp过程中需要求区间最值的情况,如果每次均遍历区间,则时间复杂度会不可接受,此时就需要单调队列进行优化;


例1:洛谷P2627 [USACO11OPEN]Mowing the Lawn G
题目链接

题目大意

在 n 头从 1 到 n 顺序编号的牛中选出一些进行工作,不能选出连续超过 k 头牛(不能连续 k+1 头牛都参与工作),每头牛都有自己的效率值 ei,求出效率和的最大值;

数据范围

0 ⩽ e i ⩽ 1 e 9 1 ⩽ n ⩽ 1 e 5 0\leqslant e_i\leqslant 1e9\\1\leqslant n \leqslant1e5 0ei1e91n1e5

构造dp方程:
d p [ 0 ] [ i ] dp[0][i] dp[0][i] 代表前 i 头牛中,i 号牛不工作产生的最大效率;
d p [ 1 ] [ i ] dp[1][i] dp[1][i] 代表前 i 头牛中,i 号牛工作产生的最大效率;

可以得出状态转移方程(sum为前缀和数组):
d p [ 0 ] [ i ] = m a x ( d p [ 0 ] [ i − 1 ] , d p [ 1 ] [ i − 1 ] ) dp[0][i]=max(dp[0][i-1],dp[1][i-1]) dp[0][i]=max(dp[0][i1],dp[1][i1])
d p [ 1 ] [ i ] = max j = i − k j < i ( d p [ 0 ] [ j ] + s u m [ i ] − s u m [ j ] ) dp[1][i]=\text{max}_{j=i-k}^{j<i}(dp[0][j]+sum[i]-sum[j]) dp[1][i]=maxj=ikj<i(dp[0][j]+sum[i]sum[j])

这种做法的时间复杂度是 O ( n k ) O(nk) O(nk),不可接受,我们便需要进行优化;

对于第二个状态转移方程,提出不变量即变为

d p [ 1 ] [ i ] = s u m [ i ] + max j = i − k j < i ( d p [ 0 ] [ j ] − s u m [ j ] ) dp[1][i]=sum[i]+\text{max}_{j=i-k}^{j<i}(dp[0][j]-sum[j]) dp[1][i]=sum[i]+maxj=ikj<i(dp[0][j]sum[j])

我们可以注意到,形式和单调队列的模板题

有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

十分类似,我们便可以通过单调队列进行优化;

维护一个从队首到队尾递减的单调队列,从队首弹出过期元素,按照单调要求从队尾加入新元素即可;

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
deque<pair<ll, int>> dqu;
ll dp[2][N], a[N];
int main()
{
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        scanf("%lld", &a[i]), a[i] += a[i - 1];
    dqu.push_front({0, 0});//初始化单调队列
    for (int i = 1; i <= n; i++)
    {
        dp[0][i] = max(dp[0][i - 1], dp[1][i - 1]);
        while (!dqu.empty() && i - dqu.front().second > k)
            dqu.pop_front();
        dp[1][i] = a[i] + ((!dqu.empty()) ? dqu.front().first : 0);
        while (!dqu.empty() && dqu.back().first < dp[0][i] - a[i])
            dqu.pop_back();
        dqu.push_back({dp[0][i] - a[i], i});
    }
    printf("%lld", max(dp[1][n], dp[0][n]));
    return 0;
}


例2:AcWing.6 多重背包问题III
题目链接

题目大意

有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值;

数据范围

0 < N ≤ 1000 0 < V ≤ 20000 0 < v i , w i , s i ≤ 20000 0<N≤1000\\ 0<V≤20000\\ 0<vi,wi,si≤20000 0<N10000<V200000<vi,wi,si20000

构造dp方程 d p [ i ] [ j ] dp[i][j] dp[i][j] 为前 i 个物品中,体积 j 所能产生的最大价值;

朴素来说,状态转移方程可以被定义为

d p [ i ] [ j ] = max k = 0 k v ⩽ j , k ⩽ s ( d p [ i − 1 ] [ j − k v ] + k w ) dp[i][j]=\text{max}_{k=0}^{kv\leqslant j,k\leqslant s}(dp[i-1][j-kv]+kw) dp[i][j]=maxk=0kvj,ks(dp[i1][jkv]+kw)

如此的时间复杂度即为 O ( N V ∑ s i ) O(NV\sum s_i) O(NVsi) ,不可接受;

我们可以发现,对于所有对 v 模数相同的 j ,比较的是相似的一组数据,我们便可以利用这一点使用一个从队首向队尾递减的单调队列在每一个 i 中优化所有对 v 模数相同的一组 j ;

具体操作上,我们可以在单调队列 dqu 中存储 { d p [ i − 1 ] [ j ] , j } \{dp[i-1][j],j\} {dp[i1][j],j} ,那么对于每一个新的 j ,单调队列的第 i 位代表的值即为 d q u [ i ] . f i r s t + ( j − d q u [ i ] . s e c o n d ) / v ⋅ w dqu[i].first+(j-dqu[i].second)/v\cdotp w dqu[i].first+(jdqu[i].second)/vw ,以此进行入队和更新dp值;

接下来进行空间优化:
我们选择优化后进行从小到大遍历 j ,这样做可以保证入队的元素均满足 d q u [ i ] . s e c o n d < = j dqu[i].second<=j dqu[i].second<=j ,为了防止同 i 的相互干扰,我们需要先将 dp[j] 值入队,再更新 dp[j] 值;

此时复杂度为 O ( N V ) O(NV) O(NV) ,2e7;

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
//deque<pair<ll,ll> > dqu;//deque被卡T
pair<ll, ll> dqu[20004]; //单调队列
int head, tail;          //单调队列首尾
ll dp[20004];
int main()
{
    ll n, t, v, w, s;
    cin >> n >> t;
    for (ll i = 1; i <= n; i++)
    {
        scanf("%lld%lld%lld", &v, &w, &s);
        for (ll j = 0; j < v; j++) //对于对v模数相同的j
        {
            head = tail = 0; //初始化单调队列
            for (ll k = j; k <= t; k += v)
            {
                while (head != tail && (k - dqu[head].second) / v > s)//避免队首元素超s
                    head++; 
                while (head != tail && (dqu[head].first + (k - dqu[head].second) / v * w) < dp[k])
                    tail--;
                dqu[tail++] = {dp[k], k};                                 //依照单调性入队
                dp[k] = dqu[head].first + (k - dqu[head].second) / v * w; //更新dp值
            }
        }
    }
    ll ans = 0;
    for (ll i = 0; i <= t; i++)
        ans = max(ans, dp[i]);
    printf("%lld", ans);
    return 0;
}

斜率优化DP

对于dp方程可化为类似 d p [ i ] = min j = 1 j < i ( k a [ i ] b [ j ] + c [ i ] + d [ j ] ) dp[i]=\text{min}_{j=1}^{j<i}(ka[i]b[j]+c[i]+d[j]) dp[i]=minj=1j<i(ka[i]b[j]+c[i]+d[j]) 形式的dp方程,朴素做法是 O ( n 2 ) O(n^2) O(n2) 的复杂度,可以通过斜率优化达到 O ( n ) O(n) O(n) 的复杂度;

对于两个 j 1 , j 2   ( j 1 < j 2 ) j_1,j_2\ (j_1<j_2) j1,j2 (j1<j2) ,假设 j 2 j_2 j2 优于 j 1 j_1 j1 ,且 b [ i ] b[i] b[i] 单增,满足决策单调性(后有说明)即意味着

k a [ i ] b [ j 1 ] + c [ i ] + d [ j 1 ] ⩾ k a [ i ] b [ j 2 ] + c [ i ] + d [ j 2 ]   k a [ i ] ⩽ d [ j 2 ] − d [ j 1 ] b [ j 1 ] − b [ j 2 ]   − k a [ i ] ⩾ d [ j 2 ] − d [ j 1 ] b [ j 2 ] − b [ j 1 ]   − k a [ i ] ⩾ s l o p e ( j 1 , j 2 ) ka[i]b[j_1]+c[i]+d[j_1]\geqslant ka[i]b[j_2]+c[i]+d[j_2]\\ \ \\ ka[i]\leqslant \frac{d[j_2]-d[j_1]}{b[j_1]-b[j_2]}\\ \ \\ -ka[i]\geqslant \frac{d[j_2]-d[j_1]}{b[j_2]-b[j_1]}\\ \ \\ -ka[i]\geqslant slope(j_1,j_2) ka[i]b[j1]+c[i]+d[j1]ka[i]b[j2]+c[i]+d[j2] ka[i]b[j1]b[j2]d[j2]d[j1] ka[i]b[j2]b[j1]d[j2]d[j1] ka[i]slope(j1,j2)

此时,若 − k a [ i ] -ka[i] ka[i] 单增,则具有决策单调性,我们可以建立一个单调队列 d q u [ i ] dqu[i] dqu[i] ,满足 s l o p e ( d q u [ i ] , d q u [ i + 1 ] ) slope(dqu[i],dqu[i+1]) slope(dqu[i],dqu[i+1]) 单增;

对于每一个新的 i ,在处理完队前斜率不满足条件的元素后,即可更新 d p [ i ] dp[i] dp[i] ,并按单调性更新队列;

对于其他问题,可以通过max/min, b [ i ] b[i] b[i] 的增减性, − k a [ i ] -ka[i] ka[i] 的增减性来决定能否以及如何构造单调队列;


例1:洛谷P3195 [HNOI2008]玩具装箱
题目链接

题目大意

P 教授决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。
P 教授有编号为 1 ⋯ n 1 \cdots n 1n 的 n 件玩具,第 i 件玩具经过压缩后的一维长度为 C i C_i Ci
为了方便整理,P教授要求:
在一个一维容器中的玩具编号是连续的。
同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物。形式地说,如果将第 i 件玩具到第 j 个玩具放到一个容器中,那么容器的长度将为 x = j − i + ∑ k = i j C k x=j-i+\sum\limits_{k=i}^{j}C_k x=ji+k=ijCk
制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为 x,其制作费用为 ( x − L ) 2 (x-L)^2 (xL)2 。其中 L 是一个常量。P 教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过 L。但他希望所有容器的总费用最小。

数据范围

1 ≤ n ≤ 5 × 1 0 4 , 1 ≤ L ≤ 1 0 7 , 1 ≤ C i ≤ 1 0 7 1≤n≤5×10 ^4 ,1 \leq L \leq 10^7 ,1 \leq C_i \leq 10^7 1n5×1041L1071Ci107

构造dp方程 d p [ i ] dp[i] dp[i] 表示前 i 个玩具装箱所需最小成本;

则有 d p [ i ] = min j = 1 j < i ( d p [ j ] + ( i − j − 1 + s u m [ i ] − s u m [ j ] − L ) 2 ) dp[i]=\text{min}_{j=1}^{j<i}(dp[j]+(i-j-1+sum[i]-sum[j]-L)^2) dp[i]=minj=1j<i(dp[j]+(ij1+sum[i]sum[j]L)2)

a [ i ] = s u m [ i ] + i , b [ i ] = s u m [ i ] + i + 1 + L , c [ i ] = a [ i ] 2 , d [ i ] = b [ i ] 2 + d p [ i ] a[i]=sum[i]+i,b[i]=sum[i]+i+1+L,c[i]=a[i]^2,d[i]=b[i]^2+dp[i] a[i]=sum[i]+i,b[i]=sum[i]+i+1+L,c[i]=a[i]2,d[i]=b[i]2+dp[i]
则有 d p [ i ] = min j = 1 j < i ( − 2 a [ i ] b [ j ] + c [ i ] + d [ j ] ) dp[i]=\text{min}_{j=1}^{j<i}(-2a[i]b[j]+c[i]+d[j]) dp[i]=minj=1j<i(2a[i]b[j]+c[i]+d[j])

此时按照上述构造单调队列即可;

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
int dqu[50004];    //单调队列
int head, tail, l; //单调队列首尾
ll dp[50004], sum[50004];
#define a(x) (sum[x] + x)
#define b(x) (sum[x] + x + 1 + l)
#define c(x) (a(x) * a(x))
#define d(x) (dp[x] + b(x) * b(x))
double slope(int j1, int j2)//计算斜率
{
    return (d(j2) - d(j1)) / (b(j2) - b(j1) == 0 ? 1e-9 : 1.0 * b(j2) - b(j1));
}

int main()
{
    ll n;
    cin >> n >> l;
    head = tail = 0;
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &sum[i]), sum[i] += sum[i - 1];
    }
    dqu[tail++] = 0;
    dqu[tail++] = 1;
    dp[1] = -2 * a(1) * b(0) + c(1) + d(0);
    for (int i = 2; i <= n; i++)
    {
        while (tail - head >= 2 && 2 * a(i) > slope(dqu[head], dqu[head + 1]))//由于a(i)递增,所以可以从前面弹出元素(决策单调性)
            head++;
        dp[i] = -2 * a(i) * b(dqu[head]) + c(i) + d(dqu[head]);//更新dp[i]
        while (tail - head >= 2 && slope(dqu[tail - 1], i) < slope(dqu[tail - 2], dqu[tail - 1]))//依据单调增维护单调队列
            tail--;
        dqu[tail++]=i;
    }
    printf("%lld",dp[n]);
    return 0;
}

ED

\

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值