线性动态规划
自认为对于我这种小菜鸡来说还是太难了,但早晚还是要迈出第一步。不管学的怎样也有一段时间了,就用几道题目做个小总结吧。
题目来源:洛谷试炼场普及组
一、导弹拦截:https://www.luogu.org/problem/P1020
题意:
这道题目的大致意思就是每台设备只能拦截高度逐渐下降的一组导弹序列,问最少需要多少台设备才能拦截到所有导弹
想法:
先考虑到如果有n发导弹呈下降形势过来,那么就需要n台设备才能全部顺利拦截。现在状态延伸出去,一组高低随机的导弹群发射过来,我们需要求出导弹的最长下降子序列使得所用设备数最少,同时我们也要求出剩余导弹的最长上升子序列表示需要补充的设备数目,相加就是最终答案。
算法实现:
这里学到了大佬的一种算法:借用 STL大法 里的upper_bound 和 lower_bound 去实现最长上升和下降子序列。
#include<algorithm>
#include<iostream>
#include<cstdio>
#define N 100000+10
using namespace std;
//存放状态
int d1[N+10], d2[N];
int len1, len2;
int a[N];
int main() {
int n=0;
while(cin>>a[++n]); n--;
len1=len2=1;
d1[len1]=d2[len2]=a[1];
for(int i=2; i<=n; i++) {
//这里找到最长下降子序列
if(d1[len1]>=a[i]) d1[++len1]=a[i];
else {
int p=upper_bound(d1+1,d1+1+len1,a[i],greater<int>())-d1;
d1[p]=a[i];
}
//同时对立的情况找到最长上升子序列
if(d2[len2]<a[i]) d2[++len2]=a[i];
else {
int q=lower_bound(d2+1, d2+1+len2, a[i])-d2;
d2[q]=a[i];
}
}
//输出结果
cout<<len1<<endl<<len2<<endl;
return 0;
}
二、尼克的任务:https://www.luogu.org/problem/P1280
题意:
大致意思就是,尼克在往后的一段时间内安排了一个任务表,但尼克并不想一直工作,不幸的是他在空闲时间只要遇到一个任务的开始就必须要去做,现在问尼克在众多任务中能空闲多上时间。
想法:
这个题目我选择的是从时间段的末尾处开始状态转移,什么意思呢,我们可以用一个计数器数组用以存放某一时间点有多少任务开始。在某一个时间会出现两种情况:
- 当前没有任务开始,那么就继承后一时间点的状态并+1:dp[i] = dp[i+1]+1
- 当前有一或多个任务开始,我们就要尝试所有的任务,找到最好的方案,状态转移方程可由任务结束时间写出:dp[i]=max(dp[i], dp[i+work[num].time])
算法实现:
#include<iostream>
#include<algorithm>
using namespace std;
struct node{
int start, time;
} work[10010];
//算子
bool cmp(node x, node y){
return x.start > y.start;
}
int n, k, sum[10010], num=1, dp[10010];
int main() {
cin>>n>>k;
//输入同时开始时间点计数
for(int i=1; i<=k; i++)
cin>>work[i].start>>work[i].time, sum[work[i].start]++;
//按开始时间排序
sort(work+1, work+k+1, cmp);
//状态转移
for(int i=n; i>=1; i--) {
if(sum[i]==0)
dp[i]=dp[i+1]+1;
else
for(int j=1; j<=sum[i]; j++){
dp[i]=max(dp[i], dp[i+work[num].time]);
num++;
}
}
cout<<dp[1]<<endl;;
return 0;
}
三、相似基因:https://www.luogu.org/problem/P1140
题意:
什么意思呢很明白了,就是给出两个基因序列,你可以在序列中加入空碱基,是的两个基因相似度最大
想法:
我们可以开一个二维的dp状态数组 ,dp[i][j]则表示第一串的前 i 位与第二串的前 j 位的匹配状态, 这样我们就可以根据加不加空碱基分出三种情况:
- 在第一串加空碱基:我们可以让第一列的最后一个碱基与第二列的倒数第二个碱基去匹配,让加入第一列的空碱基去和第二列的最后一个碱基去匹配,这样空碱基就加入了第一列的 j 位,状态转移方程:dp[i][j]=max(dp[i][j], dp[i][j-1]+v[4][b[j]])
- 在第二串加空碱基:与第一种情况对立,因此可很容易的写出状态转移方程:dp[i][j]=max(dp[i][j], dp[i-1][j]+v[a[i]][4])
- 第三种就是都不加:这种情况最简单直接匹配就行了,状态转移方程:dp[i][j]=max(dp[i][j], dp[i-1][j-1]+v[a[i]][b[j]])
算法实现:
#include<iostream>
#include<algorithm>
#include<cstdio>
#define maxn 200
using namespace std;
int dp[maxn][maxn];
int a[maxn], b[maxn];
int la, lb;
int v[5][5]={
{ 5,-1,-2,-1,-3},
{-1, 5,-3,-2,-4},
{-2,-3, 5,-2,-2},
{-1,-2,-2, 5,-1},
{-3,-4,-2,-1, 0} };
int main() {
cin>>la;
for(int i=1; i<=la; i++) {
char x; cin>>x;
if(x=='A') a[i]=0;
if(x=='C') a[i]=1;
if(x=='G') a[i]=2;
if(x=='T') a[i]=3;
}
cin>>lb;
for(int i=1; i<=lb; i++) {
char x; cin>>x;
if(x=='A') b[i]=0;
if(x=='C') b[i]=1;
if(x=='G') b[i]=2;
if(x=='T') b[i]=3;
}
for(int i=1;i<=la;i++) for(int j=1;j<=lb;j++) dp[i][j]=-2e8;
//当出现某一空串时
for(int i=1; i<=la; i++) dp[i][0]=dp[i-1][0]+v[a[i]][4];
for(int i=1; i<=lb; i++) dp[0][i]=dp[0][i-1]+v[4][b[i]];
//开始状态转移
for(int i=1; i<=la; i++) {
for(int j=1; j<=lb; j++) {
//尝试 la 加空格
//让 lb的倒 2位与 la的最后一位匹配
// lb的最后一位以加入 la的空格匹配
dp[i][j]=max(dp[i][j], dp[i][j-1]+v[4][b[j]]);
dp[i][j]=max(dp[i][j], dp[i-1][j]+v[a[i]][4]);
dp[i][j]=max(dp[i][j], dp[i-1][j-1]+v[a[i]][b[j]]);
}
}
cout<<dp[la][lb];
return 0;
}
四、多米诺骨牌:https://www.luogu.org/problem/P1282
题意:
很好理解啊,给出上下对应的两列数值,每次可以将上下对应的两个数字反转,问要想让上下数值总和相差最小,最少要反转多少次。
想法
题解区里大佬的总结很有意思啊——披着狼皮的背包!我当时看完之后有想法,但还是有些迷糊因为没有发掘到 w 和 v。瞟了一眼题解才发现:
上下最大差值和就是背包容量,单组数字的差就是物品所占体积,为了方便状态的退回与延伸,物品重量就用 1 和 -1 表示,因为有些并不是大数在上,我们可以看成已经翻过, 所以初始就是 -1,到这儿,我们最喜欢的背包就暴露出来了!——状态转移方程就不写了。
算法实现
这个源码参看了大佬的题解:https://www.luogu.org/blog/Euclid/p1282-duo-mi-nuo-gu-pai
短时间内还不能想出来其他的解决方案,而且感觉这中逻辑分析,就记录了下来。
#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 1100
#define M 6666
using namespace std;
int dp[N][M];
bool vs[N][M];
int w[N];
int v[N];
int n, tot=0, base=0;
int main(){
cin>>n;
for(int i=1; i<=n; i++) {
int x, y;
cin>>x>>y;
if(x>y) {
v[i]=2*(x-y);
w[i]=1;
tot+=x-y;
}
//等价于翻过的牌 所以base++
if(x<y) {
v[i]=2*(y-x);
w[i]=-1;
tot+=y-x;
base++;
}
}
//背包原型 dp开始
for(int i=1; i<=n; i++) {
for(int j=1; j<=tot; j++) {
//初始为上一层的状态
dp[i][j]=dp[i-1][j];
vs[i][j]=vs[i-1][j];
//状态转移
if(vs[i-1][j-v[i]] || j-v[i]==0) {
if(!vs[i][j]) {
//当前不能满足直接转移状态
dp[i][j]=dp[i-1][j-v[i]]+w[i];
vs[i][j]=1;
}
//转移状态
else
dp[i][j]=min(dp[i][j], dp[i-1][j-v[i]]+w[i]);
}
}
}
int i;
//找到第一个用所有物品可以装到的体积
for(i=tot;i>=1;i--) if(vs[n][i]) break;
cout<<dp[n][i]+base;
return 0;
}
总结
有关线性动态规划,我的理解就是从最开始的一段小区间内找到局部最优方案,然后区间逐步向外延伸,同时更新状态,直到延伸到最大就可以找出全局下的最优方案——嗯…真是个很妙的算法!