动态规划(英语:Dynamicprogramming,DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
动态规划问题满足三大重要性质:
最优子结构性质:
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
子问题重叠性质:
子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
无后效性:
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
【SSOI543】最长上升子序列 LIS
【在线测试提交传送门】
【问题描述】
一个数的序列bi,当b1 < b2 <... <bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 ≤ i1 < i2 < ... < iK ≤ N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8). 你的任务,就是对于给定的序列,求出最长上升子序列的长度。
【输入格式】
输入的第一行是序列的长度N (1≤N≤1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。
【输出格式】
最长上升子序列的长度。
【输入样例1】
7 1 7 3 5 9 4 8
【输出样例1】
4
【解题思路1】
dp[i]表示以ai为末尾的最长上升子序列的长度,而以ai结尾的最长上升子序列有两种: 1.只包含ai的子序列; 2.在满足j<i且aj<ai的以aj为结尾的上升子序列末尾,追加上ai得到的子序列。 所以有如下递推关系: dp[i]=max{1,dp[j]+1|j<i且aj<ai} 算法的时间复杂度为O(n^2)
#include<iostream>
using namespace std;
#define max(a,b) a>b?a:b
int main()
{
int n, i, j, dp[1000000+10], x[1000000+10], max_len;
while (cin >> n)
{
for (i = 0; i < n; i++)
cin >> x[i];
dp[0] = 1;//表示以x[0]为子序列最右边的长度位1
for (i = 1; i < n; i++)
{
dp[i] = 1;//初始化每种情况最小值为1
for (j = 0; j < i; j++)
{
if (x[i]>x[j] && dp[j] + 1>dp[i])//从0-i进行扫描,查找边界小于当前最优解长度相等的解优化最优解
dp[i] = dp[j] + 1;//如果允许子序列相邻元素相同 x[i]>=x[j]&&dp[j]+1>dp[i];
}
}
for (i = max_len = 0; i < n; i++)
max_len = max(max_len, dp[i]);//等到最大子序列长度
cout << max_len << endl;
}
return 0;
}
【解题思路2】
使用O(n^2)算法提交后,我们发现最后两个点超时,这里介绍O(nlogn)算法。 设dp[i]表示以i为结尾的最长递增子序列的长度,则状态转移方程为: dp[i] = max{dp[j]+1}, 1≤j<i,a[j]<a[i]. 考虑两个数a[x]和a[y],x<y且a[x]<a[y],且dp[x]=dp[y],当a[t]要选择时,到底取哪一个构成最优的呢?显然选取a[x]更有潜力,因为可能存在a[x]<a[z]<a[y],这样a[t]可以获得更优的值。在这里给我们一个启示,当dp[t]一样时,尽量选择更小的a[x]. 按dp[t]=k来分类,只需保留dp[t]=k的所有a[t]中的最小值,设g[k]记录这个值,g[k]=min{a[t],dp[t]=k}。 这时注意到g的两个特点(重点): 1. g[k]在计算过程中单调不升; 2. g数组是有序的,g[1]<g[2]<..g[n]。 利用这两个性质,可以很方便的求解: (1).设当前已求出的最长上升子序列的长度为len(初始时为1),每次读入一个新元素x: (2).若x>g[len],则直接加入到d的末尾,且len++;(利用性质2) 否则,在g中二分查找,找到第一个比x小的数g[k],并g[k+1]=x,在这里x≤g[k+1]一定成立(性质1,2)。
//解题思路2参考代码:
#include<bits/stdc++.h>
#define INF 2147483647
using namespace std;
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,tot=1,a[2000000],f[2000000];
int find(int m,int num){
int l=1,r=m,maxn;
while(r>l){
int mid=(l+r)>>1;
if(num>f[mid]) l=mid+1;
else r=mid;
}
return l;
}
int main(){
n=read();
for(int i=1;i<=n;i++) a[i]=read();
f[1]=a[1];
for(int i=2;i<=n;i++){
if(a[i]>f[tot]) f[++tot]=a[i];
else f[find(tot,a[i])]=a[i];
}
printf("%d",tot);
return 0;
}
//使用lower_bound实现二分
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
int dp[1000000+10];//dp[i]表示长度为i+1的子序列末尾元素最小值;
int a[1000000+10];
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 main()
{
int n;
n=read();
for(int i=0;i<n;i++)
{
a[i]=read();
dp[i]=INF;//不可以用memset对数组赋值INF,只能赋值0或-1;
}
for(int i=0;i<n;i++)
{
*lower_bound(dp,dp+n,a[i])=a[i];//找到>=a[i]的第一个元素,并用a[i]替换;
}
printf("%d\n",lower_bound(dp,dp+n,INF)-dp);//找到第一个INF的地址减去首地址就是最大子序列的长度;
return 0;
}
【SSoi554】渡轮问题
【在线测试提交传送门】
【问题描述】
Palmia河在某国从东向西流过,并把该国分为南北两个部分。河的两岸各有n个城市,且北岸的每一个城市都与南岸的某个城市是友好城市,而且对应的关系是一一对应的。 现在要求在二个友好城市之间建立一条航线,但由于天气的关系,所有航线都不能相交,因此,就不可能给所有的友好城市建立航线。 问题:当城市个数和友好关系建立以后,选择一种修建航线的方案,能建最多的航线而不相交。