上篇講了揹包問題算小小的入個門,這篇爲大家淺談一下線性DP的一些板子題,幫助大家理解一下動態規劃。
點這裏點這裏點這裏(數字三角形)
要找到整個數字三角形的最大值,我們可以描述爲去找從三角形第一行第一列(a[1][1])到三角形最後一行第?列(a[n][j]),這裏用問號表示我們不知道最大值具體在哪裏。
我們以從第一行第一列走到第i行第j列來爲例,能夠走到 a[i][j] 的位置只有上面 a[i-1][j] 和左上角a[i-1][j-1] 兩類,也是就是說每個位置只有對應位置的左上角和上一格來決定走到本格的最大值,同理,每個格子都滿足(a[1][1] 不滿足,因爲它是最頂上的格子)。
我們這裏設 f[i][j] 表示走到 a[i][j] 的最大值,所以根據分類原理有: f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j],其中 f[1][1] = a[1][1] // 第一個格的最大值只能是第一個數值
//自向而下
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=520;
const int INF=1e9;
int a[N][N],f[N][N];
int main()
{
int n; cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=0;j<=i+1;j++)
f[i][j]=-INF; //初始化,使得超出邊界的不影響結果,所以將其初始化無窮小
f[1][1]=a[1][1];
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
int res = -INF;
for(int i=1;i<=n;i++)
res = max(res,f[n][i]);
cout<<res<<endl;
return 0;
}
//自下而上,這個比較舒服,因爲肯定超不出邊界,即使最後一行超出去也都是0,對結果沒影響
#include<bits/stdc++.h>
using namespace std;
const int N=510;
int f[N][N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
cin>>f[i][j];
}
}
for(int i=n;i>=1;i--)
{
for(int j=i;j>=1;j--)
{
f[i][j]=max(f[i+1][j],f[i+1][j+1])+f[i][j];
}
}
cout<<f[1][1]<<endl;
}
點這裏點這裏點這裏(最長上升子序列)
下面爲大家介紹一種樸實的O(n2)的動態規劃,因爲複雜度比較高,所以只能做一些範圍較小的題。
狀態表示:f[i]表示從第一個數字開始算,以a[i]結尾的最大的上升序列。(以a[i]結尾的所有上升序列中屬性爲最大值的那一個)
狀態計算(集合劃分):j∈(0,1,2,…,i-1), 在a[i] > a[j]時,
f[i] = max(f[i], f[j] + 1)。
有一個邊界,若前面沒有比i小的,f[i]爲1(自己爲結尾)。
最後在找f[i]的最大值。
時間複雜度
O(n2)狀態數(nn) * 轉移數(nn)
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1010;
int a[N],f[N],g[N];
int main()
{
int n; cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
f[i]=1;
for(int j=1;j<i;j++)
{
if(a[i]>a[j])
f[i]=max(f[i],f[j]+1);
}
}
int res=0;
for(int i=1;i<=n;i++)
res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
優化版:
思路:首先數組a中存輸入的數(原本的數),開闢一個數組f用來存結果,最終數組f的長度就是最終的答案;假如數組f現在存了數,當到了數組a的第i個位置時,首先判斷a[i] > f[cnt] ? 若是大於則直接將這個數添加到數組f中,即f[++cnt] = a[i];這個操作時顯然的。
當a[i] <= f[cnt] 的時,我們就用a[i]去替代數組f中的第一個大於等於a[i]的數,因爲在整個過程中我們維護的數組f 是一個遞增的數組,所以我們可以用二分查找在 logn 的時間複雜的的情況下直接找到對應的位置,然後替換,即f[l] = a[i]。
我們用a[i]去替代f[i]的含義是:以a[i]爲最後一個數的嚴格單調遞增序列,這個序列中數的個數爲l個。這裏爲什麼說f數組是單調遞增的,因爲假如3位於2的前面的話,那麼能如果比3大,那麼一定比2大,所以這時候就衝突了,所以f數組一定是單調遞增的。
這樣當我們遍歷完整個數組a後就可以得到最終的結果。
時間複雜度分析:O(nlogn)
這是優化版本的動態規劃(O(nlgn)),有沒有感覺這個思想很熟悉(小編個人覺得這個想法和單調棧的想法好相似…)
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int a[N],f[N],cnt;
int find(int x)//替換掉第一個大於或者等於這個數字的那個數
int l=1; int r=cnt;
while(l<r)
{
int mid=l+r>>1;
if(f[mid]>=x)
r=mid;
else
l=mid+1;
}
return l;
}
int main()
{
int n; scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
f[++cnt]=a[1];
for(int i=2;i<=n;i++)
{
if(a[i]>f[cnt])
f[++cnt]=a[i];
else
f[find(a[i])]=a[i];
}
cout<<cnt<<endl;
return 0;
}
那麼怎麼將這個序列輸出來呢。
#include<iostream>
#include<algorithm>
#include<stdio.h>
using namespace std;
typedef long long ll;
const int N=1010;
int a[N],f[N],g[N];
int main()
{
int n; cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
f[i]=1;
g[i]=0;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
{
if(f[i]<f[j]+1)
{
f[i]=f[j]+1;
g[i]=j;
}
}
}
}
int k=1;
for(int i=1;i<=n;i++)
if(f[k]<f[i])
k=i;
cout<<f[k]<<endl;
int len=f[k];
for(int i=0;i<len;i++)
{
cout<<a[k]<<" ";
k=g[k];
}
return 0;
}
點這裏點這裏點這裏(最長公共子序列)
直接看y總的思路。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1010;
int f[N][N];
char a[N],b[N];
int n,m;
int main()
{
cin>>n>>m>>a+1>>b+1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=max(f[i-1][j],f[i][j-1]);
if(a[i]==b[j])
f[i][j]=max(f[i][j],f[i-1][j-1]+1); }
}
cout<<f[n][m]<<endl;
return 0;
}