》》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(i−1,j−v[i]) 有关(即取第 i 件商品,共使用 j 份体积的情况,是由使用前 i - 1 件物品,共使用 j - v[ i ] 份体积的情况转换来的);
在已知
f
(
i
−
1
,
x
)
(
x
∈
[
0
,
V
]
)
f(i-1,x)(x\in[0,V])
f(i−1,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(i−1,j−v[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(i−1,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(i−1,j),f(i−1,j−v[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] j−v[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(i−1,j),f(i,j−v[i])+w[i]) ;
区别仅在 f ( x , j − v [ i ] ) + w [ i ] f(x,j-v[i])+w[i] f(x,j−v[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,...,2⌊log2(s[i]+1)⌋−1,s[i]−(2⌊log2(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,2⌊log2(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]−2⌊log2(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]−2⌊log2(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<2⌊log2(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]<2⌊log2(s[i]+1)⌋+1−1
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]⩽2⌊log2(s[i]+1)⌋+1−2
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]−2⌊log2(s[i]+1)⌋+1⩽2⌊log2(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(i−1,j) ,所以只需要存储下这两组数据即可;
对于01背包,由于需要保证
j
−
v
[
i
]
j-v[i]
j−v[i] 这一项的数据是
i
−
1
i-1
i−1 时的,我们需要从后向前遍历 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]
j−v[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 n⩽16
w i ⩽ 100 w_i\leqslant100 wi⩽100
我们可以在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<<(i−1))]=min(dp[j][s∣(1<<(i−1))],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<=N⋅N)
类似地,我们可以定义出一行的状态 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[i−1][t][c−bitcount(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=∑si可行dp[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 0⩽ei⩽1e91⩽n⩽1e5
构造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][i−1],dp[1][i−1]) ;
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=i−kj<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=i−kj<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<N≤10000<V≤200000<vi,wi,si≤20000
构造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=0kv⩽j,k⩽s(dp[i−1][j−kv]+kw)
如此的时间复杂度即为 O ( N V ∑ s i ) O(NV\sum s_i) O(NV∑si) ,不可接受;
我们可以发现,对于所有对 v 模数相同的 j ,比较的是相似的一组数据,我们便可以利用这一点使用一个从队首向队尾递减的
单调队列在每一个 i 中优化所有对 v 模数相同的一组 j ;
具体操作上,我们可以在单调队列 dqu 中存储 { d p [ i − 1 ] [ j ] , j } \{dp[i-1][j],j\} {dp[i−1][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+(j−dqu[i].second)/v⋅w ,以此进行入队和更新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 1⋯n 的 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=j−i+k=i∑jCk。
制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为 x,其制作费用为 ( x − L ) 2 (x-L)^2 (x−L)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 1≤n≤5×104,1≤L≤107,1≤Ci≤107
构造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]+(i−j−1+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
\