动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法
能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠
最长上升子序列(LIS)
基本思路
在做这种类型的题目时我们需要注意
-
明确题目要求的状态
一般来说题目问什么,我们的dp[]数组就可以用来表示什么
-
状态之间的转移变换
当下状态是由那些状态转移而来的(枚举每一项操作)
-
状态的初始值和题目要求的输出
最长上升子序列
先来看一道简单的模板题
题目意思很清晰不难理解;我们需要找最长的上升子序列;
所以此时我们的dp数组就可以用来表示:以当前位结尾时最长的长度;
所以一开始的初始状态dp数组中的值都为1(自己就是自己的子序列,所以长度最小为1(但是不知道为什么用)memset
会填不上1,所以改用了fill
函数
然后来看状态的转移
我们判断这个数a[i]前面的数,如果a[j]比a[i]就说明可已接到这个数后面;此时就可以记录dp[i]=dp[j]+1
;当然前面会有很多数我们只用记录长度最大的就行(也就是dp[i]=max(dp[i],dp[j]+1)
)
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
if(a[j]<a[i])
dp[i]=max(dp[i],dp[j]+1);
}
}
最后我们找到dp数组中最大的值就可以了!
但是我们会发现上面的写法时间复杂度是O(n2),如果数据大了就会超时,所以我们就要去优化它,这里我们利用贪心的思想来优化
贪心优化(二分优化)
此时我们从上面枚举的框架中脱离,来用贪心的思想去找策略
我们要想上升子序列越长,也就需要让当前上升子序列的最后一个数尽可能小,这样接在他后面的数选择性就越多,增长子序列的可能性才越大(让局部最优变成全局最优);
因此我们可以用dp[i]去表示:当序列长度为i时,序列的最后一位数是多少;也就是用dp去存序列每位上最小的数的情况;
此时我们再次遍历初始序列(a)时就可以去判断了:
- 如果a[i]比当前dp里最后一个数大,那就说明可以增长子序列的长度,将a[i]添加到dp数组的后面,同时更新现在能构造的最长的长度len;
- 如果a[i]比当前dp里最后一个数小,我们就在dp里面去找第一个大于它的数(比如时第j个数)并将其替换;说明当子序列长度为j时,最后一位可以变成更小的a[i];(也就是上面分析的贪心的策略)
- 在这里我们构造的dp一定是有序的(因为只有大于最后一个数了才会增加长度所以dp里的数一开一递增的),所以我们去查找dp里面第一个大于它的数时就可以利用二分查找去大大优化时间;这里可以利用函数
lower_bound
(!!!不能用upper_bound
因为如果这个数在dp里面出现过那就不需要替换而upper_bound
会找到严格大于的数导致dp的状态出错!!!)当然手写也是可以的;
- 在这里我们构造的dp一定是有序的(因为只有大于最后一个数了才会增加长度所以dp里的数一开一递增的),所以我们去查找dp里面第一个大于它的数时就可以利用二分查找去大大优化时间;这里可以利用函数
(一定要清楚dp的含义,这样遍历完后数组dp的长度也是最长子序列的长度len,但是最后的dp并不是真实的子序列)
int len=0; //记录符合题意子序列的长度
for(int i=1;i<=n;i++){
if(a[i]>dp[len]) // 比当前序列最后一个数大
dp[++len]=a[i]; // 增长序列
else{
int w=lower_bound(dp+1,dp+1+len,a[i])-dp;
dp[w]=a[i]; // 找到第一个大的数的位置更新该位置的状态
}
}
最后的len就是所求的答案;优化后时间复杂度就降到了O(nlogn);
恭喜你已经初步了解,来看看实战中的运用
合唱队形
题目原文
[P1091 NOIP 2004 提高组] 合唱队形 - 洛谷
思路分析
题意不难理解;有一个初始序列;需要我们从中找到子序列去构造成中间高两边低的序列;我们可以联想到上面的最长上升子序列;也就是在每个点的位置从前往后找一个序列在从后往前找一个序列拼接起来;
那我们需要在每个位置都去找一遍吗?即使是O(nlogn)的复杂度也不够每个位置到重新去找
所以这里我们会多一步操作:记录每个位置已经能构造的最长长度;
我们只需要正着找一遍最长上升子序列再倒着找一遍最长上升子序列然后分别都记录下来;
int len=0;
for(int i=1;i<=n;i++){ //倒着找的话就变成从n到1就行
if(a[i]>b1[len])
b1[++len]=a[i];
else{
int p=lower_bound(b1+1,b1+1+len,a[i])-b1;
b1[p]=a[i];
}// 其他都是一样的
an1[i]=len; //这里用an数组记录每个位置能构造的长度
}
然后再枚举每个点当中间位置的情况:此时能构造的长度就是这个数前面和后面能构造的长度的和(还需要减一,因为中间这个被算了两次);找到最大的就是我们要求的;
int an=0;
for(int i=1;i<=n;i++) // 枚举每个人当中间那个人的情况
an=max(an,an1[i]+an2[i]-1);// 中间那个人被算了两次所以减一
cout<<n-an; // 题目要求不要的人数;所以减一下
整体代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e7;
int a[N];
int b1[N],b2[N];
int an1[N],an2[N];
signed main(){
int n;cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
int len=0;
for(int i=1;i<=n;i++){ //正着找
if(a[i]>b1[len])
b1[++len]=a[i];
else{
int p=lower_bound(b1+1,b1+1+len,a[i])-b1;
b1[p]=a[i];
}
an1[i]=len;
}
len=0;
for(int i=n;i>=1;i--){ //倒着找
if(a[i]>b2[len])
b2[++len]=a[i];
else{
int p=lower_bound(b2+1,b2+1+len,a[i])-b2;
b2[p]=a[i];
}
an2[i]=len;
}
int an=0;
for(int i=1;i<=n;i++)
an=max(an,an1[i]+an2[i]-1);
cout<<n-an;
}
参加算法竞赛!
一切的一切,都是为了补这道题
题目原文
E-参加算法竞赛!_“新华三杯”第十届成都信息工程大学ACM程序设计竞赛
思路分析
翻译一下,其实选出来的序列就是两个上升子序列拼起来的;
可以发现和上面的题目非常类似但还是有所不同
上面的题目是中间高两边低所以我们可以从两头分别找一次;但这题的两段前后都是升序;
所以我们就需要变换一下思维:在从后往前的遍历时去找最长下降子序列;这样就可以保证枚举拼接点时后面的序列是以拼接点为最小元素的上升序列啦;
代码的实现和上面类似;主要区别就是从后往前查找下降子序列;在查找时使用lower_bound
时需要加上greater<int>()
的参数让他变成查找第一个小于他的数(数组b此时也是递减的);
len=0;
b2[0]=INT_MAX; // 因为找的是下降所以先让b[0]最大
for(int i=n;i>=1;i--){
if(a[i]<b2[len]) //如果小了增长序列
b2[++len]=a[i];
else{
int p=lower_bound(b2+1,b2+1+len,a[i],greater <int>())-b2;
b2[p]=a[i];
}
an2[i]=len;
}
最后枚举原数组中每个点作为拼接点的情况,找到最大即可;
int an=0;
for(int i=1;i<=n;i++)
an=max(an,an1[i]+an2[i]-1);// 和上面一样,中间这个点被算了两次所以减1
cout<<an+1;// 因为题目允许中间存在一个断点,所以加1
整体代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e7;
int a[N];
int b1[N],b2[N];
int an1[N],an2[N];
signed main(){
int n;cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
int len=0;
for(int i=1;i<=n;i++){
if(a[i]>b1[len])
b1[++len]=a[i];
else{
int p=lower_bound(b1+1,b1+1+len,a[i])-b1;
b1[p]=a[i];
}
an1[i]=len;
}
len=0;
b2[0]=INT_MAX;
for(int i=n;i>=1;i--){
if(a[i]<b2[len])
b2[++len]=a[i];
else{
int p=lower_bound(b2+1,b2+1+len,a[i],greater <int>())-b2;
b2[p]=a[i];
}
an2[i]=len;
}
int an=0;
for(int i=1;i<=n;i++)
an=max(an,an1[i]+an2[i]-1);
cout<<an+1;
}