一.线性DP
以下是对线性
D
P
DP
DP 的个人理解。
线性
D
P
DP
DP个人感觉与递推比较相似,能一股脑的推出答案,而递推是根据数据的条件而得出的,线性
D
P
DP
DP是通过状态之间的关系来推出数值或者记录数值的,且有时可能不需要记录答案所指向的状态。不用多虑目前的状态对以后造成的影响,多想前面的状态对当前造成的影响。
包括如下例题:
- 最长上升子序列 ( L I S ) (LIS) (LIS)
- [ N O I P 2005 提高组 ] [NOIP2005 提高组] [NOIP2005提高组]过河
- 花店橱窗布置
最长上升子序列 ( L I S ) (LIS) (LIS):
一道经典的线性
d
p
dp
dp题,给出
a
a
a序列,求序列的
L
I
S
LIS
LIS。
比较直接的方法就是两层循环,外循环枚举所有点,内循环遍历该点之前的点,找到数值小于该点的点并更新状态,而
d
p
i
dp_i
dpi表示的就是以
i
i
i点结尾的
L
I
S
LIS
LIS的长度。
转移方程:
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
d
p
[
j
]
+
1
)
dp[i]=max(dp[i],dp[j]+1)
dp[i]=max(dp[i],dp[j]+1)
初始状态:
d
p
[
i
]
=
1
dp[i]=1
dp[i]=1,即开始时每个点的
L
I
S
LIS
LIS是它本身
末状态:
d
p
[
n
]
dp[n]
dp[n],比较好理解,求的当然是整个序列的
L
I
S
LIS
LIS
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
#include<iostream>
#include<algorithm>
using namespace std;
int a[1000];
int dp[1000];
int n;
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
dp[i]=1;
for(int j=i-1;j>=1;j--){
if(a[j]<a[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
cout<<dp[n]<<endl;
return 0;
}
上述方法过于暴力,我们遍历了许多没用的状态,浪费了时间。重新思考更优的解法。
若我不将答案通过状态来记录,而是在处理完所有需要处理的状态后就能得出答案。这里提供一种贪心的思路:
若我选出的最长子序列的结尾元素越小,当然越有利使序列更长
所以设
d
p
i
dp_i
dpi表示长度为
i
i
i的子序列的结尾元素。通过比较各个元素在各个不同长度的序列作为结尾元素的大小来确定最终答案。
具体地,遍历一遍数组,若大于目前结尾的元素则替换,否则往前查找第一个大于我的元素并更新状态。替换操作可以用二分来降低时间复杂度。
时间复杂度: O ( n l o g n ) O(n logn) O(nlogn)
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int len;
int a[1000];
int dp[1000];
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
dp[1]=a[1];
len=1;
for(int i=2;i<=n;i++){
if(a[i]>dp[len]){//如果大于结尾的数直接加即可
len++;
dp[len]=a[i];
continue;
}
else{
int l=0,r=len;
while(l<r){//否则二分查找
int mid=(l+r)>>1;
if(dp[mid]<a[i])l=mid+1;
else r=mid;
}
dp[l]=min(a[i],dp[l]); //更新状态
}
}
cout<<len<<endl;
return 0;
}
/
若要输出整个 L I S LIS LIS,则可以通过新开一个 p a t h path path数组记录遍历到每个数时 L I S LIS LIS的最长长度,之后再查找到每个数并倒序输出即可。
#include<iostream>
#include<algorithm>
#include<string.h>
#include<cmath>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0',ch=getchar();}
return x*f;
}
int n;
int len;
int a[1000];
int dp[1000];
int path[1000];
int res[1000];
int main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read();
dp[1]=a[1];
len=1;
path[1]=1;
for(int i=2;i<=n;i++){
if(a[i]>dp[len]){
len++;
dp[len]=a[i];
path[i]=len;
continue;
}
else{
int l=0,r=len;
while(l<r){
int mid=(l+r)>>1;
if(dp[mid]<a[i])l=mid+1;
else r=mid;
}
dp[l]=min(a[i],dp[l]);
path[i]=l;
}
}
cout<<len<<endl;
int tmp=len;
for(int i=n;i>=1;i--){
if(tmp == path[i]){
res[tmp--]=a[i];
}
}
for(int i=1;i<=len;i++)cout<<res[i]<<" ";
return 0;
}
[NOIP2005 提高组] 过河
题目描述:
在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上的一串整点: 0 , 1 , ⋯ , L 0,1,\cdots,L 0,1,⋯,L(其中 L L L 是桥的长度)。坐标为 0 0 0 的点表示桥的起点,坐标为 L L L 的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是 S S S 到 T T T 之间的任意正整数(包括 S , T S,T S,T)。当青蛙跳到或跳过坐标为 L L L 的点时,就算青蛙已经跳出了独木桥。
题目给出独木桥的长度 L L L,青蛙跳跃的距离范围 S , T S,T S,T,桥上石子的位置。你的任务是确定青蛙要想过河,最少需要踩到的石子数。
输入格式
输入共三行,
- 第一行有 1 1 1 个正整数 L L L,表示独木桥的长度。
- 第二行有 3 3 3 个正整数 S , T , M S,T,M S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离及桥上石子的个数。
- 第三行有 M M M 个不同的正整数分别表示这 M M M 个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。
输出格式
一个整数,表示青蛙过河最少需要踩到的石子数。
样例输入 #1
10
2 3 5
2 3 5 6 7
样例输出 #1
2
提示
【数据范围】
- 对于 30 % 30\% 30% 的数据, 1 ≤ L ≤ 1 0 4 1\le L \le 10^4 1≤L≤104;
- 对于 100 % 100\% 100% 的数据, 1 ≤ L ≤ 1 0 9 1\le L \le 10^9 1≤L≤109, 1 ≤ S ≤ T ≤ 10 1\le S\le T\le10 1≤S≤T≤10, 1 ≤ M ≤ 100 1\le M\le100 1≤M≤100。
【题目来源】
NOIP 2005 提高组第二题
【思路】
这一题还是多想之前的状态对现在的影响。设当前处于点 i i i,能对我造成影响的点应为 [ i − T , i − S ] [i-T,i-S] [i−T,i−S],所以只需要考虑这些点是否有石子,即:设 j ∈ [ i − T , i − S ] , f [ i ] = m i n ( f [ i − j ] + ( a [ i ] i s s t o n e ? ) ) j \in[i-T,i-S],f[i]=min(f[i-j]+(a[i]\ \ \ is \ \ \ stone?)) j∈[i−T,i−S],f[i]=min(f[i−j]+(a[i] is stone?))
#include<iostream>
#include<algorithm>
#include<string.h>
#define int long long
using namespace std;
const int N=1e7+10;
int L,S,T,n;
int a[N],f[N],tot;
int dis[N],dp[N];
signed main(){
cin>>L>>S>>T>>n;
if(S==T){
int ans=0;
for(int i=1;i<=n;i++){cin>>x;if(x%S==0)ans++;}
cout<<ans<<endl;
return 0;
}
for(int i=1;i<=n;i++)cin>>a[i],f[a[i]]=1;
sort(a+1,a+1+n);
for(int i=1;i<=L+10;i++){
dp[i]=1e9;
for(int j=S;j<=T;j++)if(i>=j)dp[i]=min(dp[i],dp[i-j]+f[i]);
}
int ans=1e9;
for(int i=L;i<=L+10;i++)
ans=min(ans,dp[i]);
cout<<ans<<endl;
return 0;
}
成功的
R
E
RE
RE了,数据范围什么的是一点没看。很明显开不了
1
e
9
1e9
1e9的数组,就要想一些其它的优化了。本蒟蒻根本不会。看了题解,原来是有奇奇怪怪的性质:借鉴了该大犇的博客。
总结了一下就是:当两个石头的距离超过了
l
c
m
(
S
,
T
)
lcm(S,T)
lcm(S,T),再把距离保存其实是没有意义的,因为只要距离大于l
l
c
m
(
S
,
T
)
lcm(S,T)
lcm(S,T),以后的点都可以被覆盖。
啊我还是太菜了,按照大犇的思路改的代码:
#include<iostream>
#include<algorithm>
#include<string.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0',ch=getchar();}
return x*f;
}
const int N=2e6+10;
int L,S,T,n;
int a[N],f[N],tot;
int dis[N],dp[N];cx,l
int gcd(int a,int b){
if(b==0)return a;
return gcd(b,a%b);
}
int _lcm(int a,int b){
return a*b/gcd(a,b);
}
signed main(){
L=read();
S=read(),T=read(),n=read();
int lcm=_lcm(S,T);
if(S==T){
int ans=0;
for(int i=1;i<=n;i++){
int x=read();
if(x%S==0)ans++;
}
cout<<ans<<endl;
return 0;
}
for(int i=1;i<=n;i++)a[i]=read();
sort(a+1,a+1+n);
dis[n+1]=min(L-a[n],lcm);
for(int i=1;i<=n;i++){
dis[i]=min(a[i]-a[i-1],lcm);
tot+=dis[i];
f[tot]=1;
}
tot+=dis[n+1];
for(int i=1;i<=tot+10;i++){
dp[i]=1e18;
for(int j=S;j<=T;j++){
if(i>=j)dp[i]=min(dp[i],dp[i-j]+f[i]);
}
}
int ans=1e18;
for(int i=tot;i<=tot+10;i++){
ans=min(ans,dp[i]);
}
cout<<ans<<endl;
}
花店橱窗布置
题目描述
某花店现有 F F F 束花,每一束花的品种都不一样。至少有同样数量的花瓶,被按顺序摆成一行。花瓶的位置是固定的,从左到右按 1 ∼ V 1\sim V 1∼V 顺序编号, V V V 是花瓶的数目。
花束可以移动,并且每束花用 1 ∼ F 1\sim F 1∼F 的整数标识。所有花束在放入花瓶时必须保持其标识数的顺序。例如,假设杜鹃花的标识数为 1 1 1,秋海棠的标识数为 2 2 2,康乃馨的标识数为 3 3 3,即杜鹃花必须放在秋海棠左边的花瓶中,秋海棠必须放在康乃馨左边的花瓶中。每个花瓶只能放一束花。
每个花瓶的形状和颜色也不相同,因此,当各个花瓶中放入不同的花束时,会产生不同的美学效果,并以美学值(一个整数 a i , j a_{i,j} ai,j)来表示,空置花瓶的美学值为 0 0 0。在上述的例子中,花瓶与花束的不同搭配所具有的美学值,可以用如下的表格来表示:
花瓶 1 | 花瓶 2 | 花瓶 3 | 花瓶 4 | 花瓶 5 | |
---|---|---|---|---|---|
杜鹃花 | 7 7 7 | 23 23 23 | − 5 -5 −5 | − 24 -24 −24 | 16 16 16 |
秋海棠 | 5 5 5 | 21 21 21 | − 4 -4 −4 | 10 10 10 | 23 23 23 |
康乃馨 | − 21 -21 −21 | 5 5 5 | − 4 -4 −4 | − 20 -20 −20 | 20 20 20 |
根据表格,杜鹃花放在花瓶 2 2 2 中,会显得非常好看,但若放在花瓶 4 4 4 中,则显得很难看。
为了取得最佳的美学效果,必须在保持花束顺序的前提下,使花的摆放取得最大的美学值,如果具有最大美学值的摆放方式不止一种,则输出任何一种方案即可。
输入格式
输入文件的第一行是两个整数 F F F 和 V V V,分别为花束数和花瓶数。
接下来是矩阵 a i , j a_{i,j} ai,j,共 F F F 行,每行 V V V 个整数, a i , j a_{i,j} ai,j 表示花束 i i i 摆放在花瓶 j j j 中的美学值。
输出格式
输出文件的第一行是一个整数,为最大的美学值;接下来一行 F F F 个整数,为那束花放入那个花瓶的编号。
样例输入 #1
3 5
7 23 -5 -24 16
5 21 -4 10 23
-21 5 -4 -20 20
样例输出 #1
53
2 4 5
提示
对于 100 % 100\% 100% 的数据, 1 ≤ F ≤ V ≤ 100 1\le F\le V\le 100 1≤F≤V≤100。
思路:
思路来自@WAWA鱼dalao
观察数据范围较小,想到用
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示枚举到第
i
i
i朵花用了
j
j
j个橱窗时的最优解。
当
i
=
j
i=j
i=j时,若再开一个橱窗,下一朵花必定在这个橱窗。
否则,下一朵花客房可放可不放在这个橱窗中。
即:
i
f
(
i
=
j
)
,
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
−
1
]
+
a
[
i
]
[
j
]
if(i=j),f[i][j]=f[i-1][j-1]+a[i][j]
if(i=j),f[i][j]=f[i−1][j−1]+a[i][j]
e
l
s
e
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
−
1
]
,
f
[
i
−
1
]
[
j
−
1
]
+
a
[
i
]
[
j
]
)
else\ \ f[i][j]=max(f[i][j-1],f[i-1][j-1]+a[i][j])
else f[i][j]=max(f[i][j−1],f[i−1][j−1]+a[i][j])
#include<iostream>
#include<algorithm>
#include<string.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0',ch=getchar();}
return x*f;
}
const int N=1e3+10;
int a[N][N],f[N][N];
int n,m;
void dfs(int i,int j){
if(i==0 || j==0)return;
while(f[i][j]==f[i][j-1])j--;
dfs(i-1,j-1);
cout<<j<<" ";
}
signed main(){
n=read(),m=read();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=i;j<=m;j++){
if(i==j){
f[i][j]=f[i-1][j-1]+a[i][j];
}
else f[i][j]=max(f[i][j-1],f[i-1][j-1]+a[i][j]);
}
}
cout<<f[n][m]<<endl;
dfs(n,m);
return 0;
}