注:这是dp套路整理里面题的题解qwq
一、简单dp
1.1 快速幂优化dp
1.1.1 模板题 斐波那契数列
大家都知道,斐波那契数列是满足如下性质的一个数列:
F
n
=
{
1
(
n
≤
2
)
F
n
−
1
+
F
n
−
2
(
n
>
2
)
F_n=\left\{ \begin{array}{lr} 1\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (n\le 2) \\ F_{n-1}+F_{n-2}\ \ \ \ \ \ \ \ \ (n>2) \\ \end{array} \right.
Fn={1 (n≤2)Fn−1+Fn−2 (n>2)
请你求出
F
n
m
o
d
1
0
9
+
7
(
1
≤
n
<
2
63
)
F_n\ \rm mod\ 10^9+7(1\le n<2^{63})
Fn mod 109+7(1≤n<263)的值。
构造出矩阵快速幂转移矩阵:
[
F
n
F
n
−
1
]
=
[
1
1
1
0
]
∗
[
F
n
−
1
F
n
−
2
]
\begin{bmatrix}F_{n}\\F_{n-1}\end{bmatrix}=\begin{bmatrix}1 & 1\\1 & 0\end{bmatrix}*\begin{bmatrix}F_{n-1}\\F_{n-2}\end{bmatrix}
[FnFn−1]=[1110]∗[Fn−1Fn−2]
于是有
[
F
n
F
n
−
1
]
=
[
1
1
1
0
]
n
−
2
∗
[
F
2
F
1
]
=
[
1
1
1
0
]
n
−
2
∗
[
1
1
]
\begin{bmatrix}F_{n}\\F_{n-1}\end{bmatrix}=\begin{bmatrix}1 & 1\\1 & 0\end{bmatrix}^{n-2}*\begin{bmatrix}F_{2}\\F_{1}\end{bmatrix}=\begin{bmatrix}1 & 1\\1 & 0\end{bmatrix}^{n-2}*\begin{bmatrix}1\\1\end{bmatrix}
[FnFn−1]=[1110]n−2∗[F2F1]=[1110]n−2∗[11]
于是可以用矩阵快速幂求解
F
n
F_n
Fn的值。
代码略
1.1.2 模板题 zyd的妹子其二
zyd要妥善安排他的后宫,他想在机房摆一群妹子,一共有
n
n
n个位置排成一排,每个位置可以摆妹子也可以不摆妹子。有些类型妹子如果摆在相邻的位置(隔着一个空的位置不算相邻),就不好看了。假定每种妹子数量无限,求摆妹子的方案数。
输入有
m
+
1
m+1
m+1行,第一行有两个用空格隔开的正整数n、m,m表示妹子的种类数。接下来的
m
m
m行,每行有
m
m
m个0/1字符,第
i
i
i行第
j
j
j列为
a
i
j
a_{ij}
aij。若
a
i
j
a_{ij}
aij为1,则表示第
i
i
i种妹子第
j
j
j种妹子不能排在相邻的位置,输入保证对称。
n
≤
1
0
9
,
m
≤
100
n\le 10^9,m\le 100
n≤109,m≤100
令
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示放了
i
i
i个妹子,最后一个妹子是
j
j
j的方案数。
d
p
[
i
]
[
j
]
=
∑
a
j
k
=
=
0
d
p
[
i
−
1
]
[
k
]
dp[i][j]=\sum_{a_{jk}==0} dp[i-1][k]
dp[i][j]=∑ajk==0dp[i−1][k]
令
b
i
j
=
1
−
a
i
j
b_{ij}=1-a_{ij}
bij=1−aij
于是得到
[
d
p
[
i
]
[
m
]
d
p
[
i
]
[
m
−
1
]
d
p
[
i
]
[
m
−
2
]
.
.
.
d
p
[
i
]
[
1
]
]
=
[
b
11
b
12
b
13
.
.
.
b
1
m
b
21
b
22
b
23
.
.
.
b
2
m
b
31
b
32
b
33
.
.
.
b
3
m
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
b
m
1
b
m
2
b
m
3
.
.
.
b
m
m
]
∗
[
d
p
[
i
−
1
]
[
m
]
d
p
[
i
−
1
]
[
m
−
1
]
d
p
[
i
−
1
]
[
m
−
2
]
.
.
.
d
p
[
i
−
1
]
[
1
]
]
\begin{bmatrix}dp[i][m]\\dp[i][m-1]\\dp[i][m-2]\\...\\dp[i][1]\end{bmatrix}=\begin{bmatrix}b_{11} & b_{12} & b_{13} & ... & b_{1m}\\b_{21} & b_{22} &b_{23} & ... & b_{2m}\\b_{31} & b_{32} & b_{33} & ... & b_{3m}\\ ... & ... & ... & ... & ...\\ b_{m1} & b_{m2} & b_{m3} & ... & b_{mm}\end{bmatrix}*\begin{bmatrix}dp[i-1][m]\\dp[i-1][m-1]\\dp[i-1][m-2]\\...\\dp[i-1][1]\end{bmatrix}
⎣⎢⎢⎢⎢⎡dp[i][m]dp[i][m−1]dp[i][m−2]...dp[i][1]⎦⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎡b11b21b31...bm1b12b22b32...bm2b13b23b33...bm3...............b1mb2mb3m...bmm⎦⎥⎥⎥⎥⎤∗⎣⎢⎢⎢⎢⎡dp[i−1][m]dp[i−1][m−1]dp[i−1][m−2]...dp[i−1][1]⎦⎥⎥⎥⎥⎤
代码和剩下的步骤略。
1.2 LIS & LCS的优化
1.2.1 模板题 LIS
给定一长度为 n n n的数列,请在不改变原数列顺序的前提下,从中随机的取出一定数量的整数,并使这些整数构成单调上升序列。 输出这类单调上升序列的最大长度。 n ≤ 1 0 5 n\le 10^5 n≤105。
f i = max j = 1 i − 1 { f j + 1 } , a j < a i f_i=\max_{j=1}^{i-1}\{f_j+1\},a_j<a_i fi=maxj=1i−1{fj+1},aj<ai。离散化后用树状数组记录下 ≤ x \le x ≤x的 f f f的最大值,遍历求解。
二、背包
2.1 01背包(略)
2.2 完全背包(略)
2.3 多重背包
2.3.1 模板题 宝物筛选
小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。
小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为
W
W
W 的采集车,洞穴里总共有
n
n
n 种宝物,每种宝物的价值为
v
i
v_i
vi,重量为
w
i
w_i
wi,每种宝物有
m
i
m_i
mi 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
n
≤
100
,
∑
m
i
≤
1
0
5
,
0
≤
W
≤
4
∗
1
0
4
n\le 100,\sum m_i\le 10^{5},0\le W\le 4*10^4
n≤100,∑mi≤105,0≤W≤4∗104。
令
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示前
i
i
i种宝物,目前载重为
j
j
j的最大价值。
转移方程:
d
p
[
i
]
[
j
]
=
max
{
d
p
[
i
−
1
]
[
j
−
k
∗
w
i
]
+
k
∗
v
i
}
dp[i][j]=\max\{dp[i-1][j-k*w_i]+k*v_i\}
dp[i][j]=max{dp[i−1][j−k∗wi]+k∗vi}。
注意到对于每个
i
i
i,
j
j
j和
j
−
k
∗
w
i
j-k*w_i
j−k∗wi是模
w
i
w_i
wi同余的。也就是说
j
j
j只会被与
j
j
j模
w
i
w_i
wi相同的数影响。
设
j
=
p
∗
w
i
+
r
j=p*w_i+r
j=p∗wi+r,则转移方程可化为
d
p
[
i
]
[
j
]
=
max
{
d
p
[
i
−
1
]
[
k
∗
w
i
+
r
]
+
(
p
−
k
)
∗
v
i
}
(
p
−
m
i
≤
k
≤
p
)
dp[i][j]=\max\{dp[i-1][k*w_i+r]+(p-k)*v_i\}(p-m_i \le k\le p)
dp[i][j]=max{dp[i−1][k∗wi+r]+(p−k)∗vi}(p−mi≤k≤p)
拆开
p
∗
v
i
p*v_i
p∗vi这项常数,得到
d
p
[
i
]
[
j
]
=
max
{
d
p
[
i
−
1
]
[
k
∗
w
i
+
r
]
−
k
∗
v
i
}
+
p
∗
v
i
(
p
−
m
i
≤
k
≤
p
)
dp[i][j]=\max\{dp[i-1][k*w_i+r]-k*v_i\}+p*v_i(p-m_i\le k\le p)
dp[i][j]=max{dp[i−1][k∗wi+r]−k∗vi}+p∗vi(p−mi≤k≤p)
发现这就是一个滑块窗口,所以用单调队列乱搞。
#include<stdio.h>
#include<cstring>
#include<algorithm>
#define re register int
using namespace std;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
const int Size=100005;
int n,V,v[Size],w[Size],num[Size],dp[Size];
int hd,tl;
struct node {
int id,val;
} Queue[Size];
inline void push(int id,int val) {
while(hd<=tl && val>Queue[tl].val) tl--;
Queue[++tl].id=id;
Queue[tl].val=val;
}
inline void pop(int id,int num) {
while(hd<=tl && Queue[hd].id+num<id) hd++;
}
int main() {
n=read();
V=read();
int ans=0;
for(re i=1; i<=n; i++) {
v[i]=read();
w[i]=read();
num[i]=read();
if(!w[i]) {
n--;
ans+=v[i]*num[i];
}
}
for(re i=1; i<=n; i++) {
for(re r=0; r<w[i]; r++) {
int lim=(V-r)/w[i];
hd=1,tl=0;
for(re p=0; p<=lim; p++) {
push(p,dp[p*w[i]+r]-p*v[i]);
pop(p,num[i]);
dp[p*w[i]+r]=max(dp[p*w[i]+r],Queue[hd].val+p*v[i]);
}
}
}
printf("%d",dp[V]);
return 0;
}
2.3.2 例题 shopping
马上就是小苗的生日了,为了给小苗准备礼物,小葱兴冲冲地来到了商店街。商店街有
n
n
n个商店,并且它们之间的道路构成了一颗树的形状。
第
i
i
i个商店只卖第
i
i
i种物品,小苗对于这种物品的喜爱度是
w
i
w_i
wi,物品的价格为
c
i
c_i
ci,物品的库存是
d
i
d_i
di。但是商店街有一项奇怪的规定:如果在商店
u
,
v
u,v
u,v买了东西,并且有一个商店
w
w
w在
u
u
u到
v
v
v的路径上,那么必须要在商店
w
w
w买东西。小葱身上有
m
m
m元钱,他想要尽量让小苗开心,所以他希望最大化小苗对买到物品的喜爱度之和。这种小问题对于小葱来说当然不在话下,但是他的身边没有电脑,于是他打电话给同为OI选手的你,你能帮帮他吗?
n
≤
500
,
m
≤
4000
,
w
i
≤
4000
,
d
i
≤
100
n\le 500,m\le 4000,w_i\le 4000,d_i\le 100
n≤500,m≤4000,wi≤4000,di≤100
若在
u
,
v
u,v
u,v买了东西,那么
u
,
v
u,v
u,v路径之间的所有点都要买东西,也就是说买东西的节点构成了树上连通块。果断用点分治。
题目显然是一个多重背包,因此用dfs序转移+单调队列优化多充背包即可。
代码懒得写……
2.4 分组背包
2.4.1 例题 [HNOI2007]梦幻岛宝珠
给你N颗宝石,每颗宝石都有重量和价值。要你从这些宝石中选取一些宝石,保证总重量不超过W,且总价值最大,并输出最大的总价值。数据范围: N ⩽ 100 , W ⩽ 2 30 N⩽100,W⩽2^{30} N⩽100,W⩽230。每颗宝石的重量可以表示为 a ∗ 2 b ( a ⩽ 10 ; b ⩽ 30 ) a*2^b(a\leqslant 10;b\leqslant 30) a∗2b(a⩽10;b⩽30)。
直接背包显然容量太大了,注意到保证每颗宝石的重量可以表示为
a
∗
2
b
(
a
≤
10
,
b
≤
30
)
a*2^b(a\le10,b\le30)
a∗2b(a≤10,b≤30)的形式,考虑把
b
b
b相同的宝石分为一组,进行分组背包。
假设第
i
i
i组宝石的重量都可以表示为
a
∗
2
i
a*2^i
a∗2i,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示前
i
i
i组宝石,且所用重量不超过
j
∗
2
i
j*2^{i}
j∗2i得到的最大价值。
先做一次dp预处理出组内子状态信息。然后组见合并的dp方程:
d
p
[
i
]
[
j
]
=
max
{
d
p
[
i
]
[
j
−
k
]
+
d
p
[
i
−
1
]
[
m
i
n
(
s
u
m
[
i
−
1
]
,
2
k
+
(
W
2
i
−
1
&
1
)
)
]
}
dp[i][j]=\max\{dp[i][j-k]+dp[i-1][min(sum[i-1],2k+(\large\frac{W}{2^{i-1}}\normalsize \&1))]\}
dp[i][j]=max{dp[i][j−k]+dp[i−1][min(sum[i−1],2k+(2i−1W&1))]}。
后面这一串的意义:第
i
i
i组用了
(
j
−
k
)
∗
2
i
(j-k)*2^i
(j−k)∗2i的重量,则第
i
−
1
i-1
i−1组用了
k
∗
2
i
=
2
k
∗
2
i
−
1
k*2^i=2k*2^{i-1}
k∗2i=2k∗2i−1的重量。但如果只算
d
p
[
i
−
1
]
[
2
k
]
dp[i-1][2k]
dp[i−1][2k],最后的总重量实际上是
2
⌊
l
o
g
2
W
⌋
2^{\lfloor log_2W\rfloor}
2⌊log2W⌋的,所以应该要在中间加上
W
W
W后面二进制位的重量。而
W
2
i
−
1
&
1
\large\frac{W}{2^{i-1}}\normalsize \&1
2i−1W&1表示的是
W
W
W的第
i
−
1
i-1
i−1位的值。加上这一位转移,最后的答案就是
d
p
[
l
o
g
2
W
]
[
1
]
dp[log_2W][1]
dp[log2W][1]。
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#include<vector>
#define re register int
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
inline void write(int x) {
if(x>9) write(x/10);
putchar(x%10+'0');
}
const int Size=105;
const int Maxb=35;
int n,W,w[Size],v[Size],sum[Size];
vector<int> a[Maxb];
int dp[Maxb][Size];
int main() {
while(scanf("%d%d",&n,&W)==2 && n!=-1) {
memset(dp,0,sizeof(dp));
memset(sum,0,sizeof(sum));
for(re i=0; i<=31; i++) a[i].clear();
int maxb=0;
for(re i=1; i<=n; i++) {
w[i]=read();
v[i]=read();
int b=0;
while(!(w[i]&1)) {
b++;
w[i]>>=1;
}
a[b].push_back(i);
if(b>maxb) maxb=b;
sum[b]+=w[i];
}
for(re i=0; i<=maxb; i++) {
int len=a[i].size();
for(re j=0; j<len; j++) {
for(re k=sum[i]; k>=w[a[i][j]]; k--) {
dp[i][k]=max(dp[i][k],dp[i][k-w[a[i][j]]]+v[a[i][j]]);
}
}
}
int logW=log2(W);
for(re i=1; i<=logW; i++) {
sum[i]+=(sum[i-1]+1)>>1;
for(re j=sum[i]; j>=0; j--) {
for(re k=0; k<=j; k++) {
dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[i-1][min(sum[i-1],(k<<1)|(W>>(i-1)&1))]);
}
}
}
printf("%d\n",dp[logW][1]);
}
return 0;
}
三、区间dp
3.1 朴素区间dp(略)
3.2 断环为链(略)
3.3 四边形不等式优化
3.3.1 例题 [NOI1995]石子合并
显然是一个区间dp,最小得分方程为
d
p
[
i
]
[
j
]
=
min
{
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
s
u
m
(
i
,
j
)
}
dp[i][j]=\min\{dp[i][k]+dp[k+1][j]+sum(i,j)\}
dp[i][j]=min{dp[i][k]+dp[k+1][j]+sum(i,j)}。满足四边形不等式,断环为链后用四边形不等式优化。
最大得分无法用四边形不等式优化,但是
[
l
,
r
]
[l,r]
[l,r]最大得分只能在
k
=
l
k=l
k=l或
k
+
1
=
r
k+1=r
k+1=r时取到。
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#include<vector>
#define re register int
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
inline void write(int x) {
if(x>9) write(x/10);
putchar(x%10+'0');
}
const int Size=205;
const int INF=0x3f3f3f3f;
int n,a[Size],sum[Size];
int dpmax[Size][Size],dpmin[Size][Size],s[Size][Size];
int main() {
memset(dpmin,0x3f,sizeof(dpmin));
n=read();
for(re i=1; i<=n; i++) a[i]=a[i+n]=read();
for(re i=1; i<=(n<<1); i++) {
sum[i]=sum[i-1]+a[i];
dpmax[i][i]=dpmin[i][i]=0;
s[i][i]=i;
}
for(re len=2; len<=n; len++) {
const int maxl=(n<<1)-len+1;
for(re l=1; l<=maxl; l++) {
const int r=l+len-1,val=sum[r]-sum[l-1];
dpmax[l][r]=max(dpmax[l][r-1],dpmax[l+1][r])+val;
int minn=INF,id=0;
for(re k=s[l][r-1]; k<=s[l+1][r]; k++) {
if(dpmin[l][k]+dpmin[k+1][r]+val<minn) {
minn=dpmin[l][k]+dpmin[k+1][r]+val;
id=k;
}
}
dpmin[l][r]=minn;
s[l][r]=id;
}
}
int ansmax=0,ansmin=INF;
for(re i=1; i<=n; i++) {
ansmax=max(ansmax,dpmax[i][i+n-1]);
ansmin=min(ansmin,dpmin[i][i+n-1]);
}
printf("%d\n%d",ansmin,ansmax);
return 0;
}
四、状压dp
4.1 朴素状压dp
4.1.1 模板题 关灯问题II
水题,记忆化bfs就可以了。
另外吐槽一下,洛咕上面题解都写丑了,每次按按钮不用
O
(
n
)
O(n)
O(n)更新,用小技巧可以做到
O
(
1
)
O(1)
O(1)。
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#include<vector>
#define re register int
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
inline void write(int x) {
if(x>9) write(x/10);
putchar(x%10+'0');
}
const int Size=105;
const int Maxn=2055;
const int INF=0x3f3f3f3f;
int n,m,a[Size],b[Size],Queue[Maxn],dis[Maxn];
bool vis[Maxn];
int bfs() {
int hd=0,tl=0;
vis[Queue[++tl]=(1<<n)-1]=true;
while(hd<tl) {
int x=Queue[++hd];
for(re i=1; i<=m; i++) {
int val=x&a[i]|b[i];
if(!vis[val]) {
vis[val]=true;
Queue[++tl]=val;
dis[val]=dis[x]+1;
if(!val) {
return dis[val];
}
}
}
}
return -1;
}
int main() {
n=read();
m=read();
for(re i=1; i<=m; i++) {
for(re j=1; j<=n; j++) {
int val=read();
if(val!=1) a[i]^=1<<(j-1);
if(val==-1) b[i]^=1<<(j-1);
}
}
printf("%d",bfs());
return 0;
}
4.1.2 模板题 [SCOI2007]排列
也是水题……