此文章为算法设计与分析动态规划章节学习总结.....
1.基本思想
将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同,适合于用动态规划求解的问题经分解得到子问题往往不是互相独立的。
2.基本要素
(1)最优子结构性质:
原问题的最优解包含其子问题的最优解。问题的最优子结构性质,提供了该问题可用动态规划算法求解的重要线索。
(2)重叠子问题性质:
在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此问题时,只需要用常数时间查一下表格。从而可以获得较高的效率。
3.部分问题思路
(1)矩阵连乘问题
给定n个矩阵{A1,A2,...,An},其中Ai与Ai+1是可乘的,i=1,2...,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序 计算矩阵连乘积需要的数乘次数最少。
输入数据:共m+1行;第一行为测试数据的组数m;以后每行n+1个正 整数,表示n个矩阵的行列值。
输出:最少次数及连乘的计算次序。
样例输入:
1
5 10 4 6 10 2
样例输出:
348
(A1(A2(A3(A4A5))))
思路:
设计算A[i:j],1≤i≤j≤n,所需要的最少数乘次数m[i,j],则原问题的最优值为m[1,n]。
当i=j时,A[i:j]=Ai,因此,m[i][i]=0,i=1,2,…,n
当i<j时,若A[i:j]的最优次序在Ak和Ak+1之间断开,即A[i:j]=A[i:k]*A[k+1][j],i<=k<j,则:m[i][j]=m[i][k]+m[k+1][j]+pi-1pkpj。故枚举k所能出现的位置得到m[i][j]的最优值。
得递推关系:
m[i][j] = 0 when i=j
m[i][j] = min{m[i][k]+m[k+1][j]+p[i-1]p[k]p[j]} when i<j,i<=k<j
构造最优解:在每次更新m[i][j]时顺带维护一个s[i][j]来记录A[i:j]的分裂点。输出时通过s[i][j]将问题分解成两个子问题分别求解。 |
#include<iostream>
#include<algorithm>
#include<sstream>
#include<string>
#include<cstring>
using namespace std;
const int N=110;
int f[N][N],s[N][N];
int p[N];
void solve(int l,int r){
if(l==r) cout<<"A"<<l;
else{
int k=s[l][r];
cout<<"(";
solve(l,k);
cout<<"*";
solve(k+1,r);
cout<<")";
}
}
int main(){
int n;
int t;
cin>>t;
getchar();
while(t--){
memset(f,0,sizeof(f));
memset(p,0,sizeof(p));
int cnt=0;
char sa[10000];
string str;
gets(sa);
stringstream ss(sa);
while(ss>>str){
int t=stoi(str);
p[cnt]=t;
cnt++;
}
n=cnt-1;
for(int i=1;i<=n;i++) f[i][i]=0;
for(int r=2;r<=n;r++){
for(int i=1;i+r-1<=n;i++){
int j=i+r-1;
f[i][j]=f[i+1][j]+p[i-1]*p[i]*p[j];
s[i][j]=i;
for(int k=i+1;k<j;k++){
int t=f[i][k]+f[k+1][j]+p[i-1]*p[k]*p[j];
if(t<f[i][j]){
f[i][j]=t;
s[i][j]=k;
}
}
}
}
cout<<f[1][n]<<endl;
solve(1,n);
cout<<endl;
}
}
(2) 数字三角形
给定一个由n行数字组成的数字三角形,如下图所示:
试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大(每一步只能从一个数走到下一层上和它最近的左边的数或者右边的数)。
输入数据:
第一行是数字三角形的行数,接下来 n 行是数字三角形中的数字。
思路: 设f[i][j]为从三角形的底走到 a[i][j]的最大值,则对于每个点从左下方或右下方较大的f[i][j]中找到较大者加上自身,直到f[1][1]. 状态转移方程: f [i][j] = a[i][j] when i=n f [i][j] = Max(f[i+1][j],f[i+1][j+1])+a[i][j] when i<n 求路径则是从1,1向下判断f[i+1][j]和f[i+1][j+1]谁更大,便是从哪里来的。 |
包含路径代码:
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
const int N=110;
int f[N][N],b[N][N];
int s1[N];
string s2[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>f[i][j];
b[i][j]=f[i][j];
}
}
for(int i=n-1;i>=1;i--){
for(int j=1;j<=i;j++){
if(f[i+1][j]>f[i+1][j+1]){
f[i][j]=f[i+1][j]+f[i][j];
}
else{
f[i][j]=f[i+1][j+1]+f[i][j];
}
}
}
cout<<f[1][1]<<endl;
int j=1;
for(int i=1;i<n;i++){
if(f[i+1][j]>f[i+1][j+1]){
s1[i]=b[i+1][j];
s2[i]="↙";
}
else {
s1[i]=b[i+1][j+1];
s2[i]="↘";
j++;
}
}
cout<<b[1][1]<<endl;
for(int i=1;i<n;i++){
cout<<s2[i]<<s1[i]<<endl;
}
}
(3)最长上升子序列
输入:
第一行给出一个整数N(0<N<100)表示待测数据组数。接下来每组数据两行,分别为待测的两组字符串。每个字符串长度不大于1000.。
输出:
每组测试数据输出一个整数,表示最长公共子序列长度。每组结果占一行。
样例输入
2
asdf
adfsd
123abc
abc123abc
样例输出
3
6
思路:设两序列A和T,长度分别为n,m,A与T的最长公共子序列为S,长度为k,则: 若am=tn,则sk=am=tn且Sk-1是Am-1和Tn-1的最长公共子序列; 得到状态转移方程: f[i,j] = 0, i=0或j=0 最优解用辅助容器记录路径,回溯输出既可。 |
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
#include<vector>
using namespace std;
const int N=1010;
int f[N][N];
int p[N][N]; //1 ↖ 2 ↑ 3 ←
void solve(char a[],char b[],int i,int j){
if(i==0||j==0) return ;
if(p[i][j]==2) solve(a,b,i-1,j);
if(p[i][j]==3) solve(a,b,i,j-1);
if(p[i][j]==1) {
solve(a,b,i-1,j-1);
cout<<a[i];
}
}
void fun(char a[],char b[]){
int la=strlen(a+1);
int lb=strlen(b+1);
// cout<<a+1<<endl<<b+1<<endl;
// memset(f,0,sizeof(f));
for(int i=1;i<=la;i++){
for(int j=1;j<=lb;j++){
// f[i][j]=f[i-1][j-1];
// f[i][j]=max(f[i-1][j],f[i][j-1]); //这两个一定比f[i-1][j-1]不小
if(a[i]==b[j]) {
f[i][j]=f[i-1][j-1]+1;
p[i][j]=1;
}
else{
if(f[i-1][j]>f[i][j-1]) {
f[i][j]=f[i-1][j];
p[i][j]=2;
}
else{
f[i][j]=f[i][j-1];
p[i][j]=3;
}
}
}
}
cout<<f[la][lb]<<endl;
solve(a,b,la,lb);
cout<<endl;
}
int main(){
int t;
cin>>t;
while(t--){
char a[N],b[N];
cin>>a+1>>b+1;
fun(a,b);
}
}
(4)0-1背包问题
输入:由文件input.txt给出输入数据,第一行有两个正整数n和W,n是物品种数,W是背包容量,接下来的一行中有n个正整数,表示物品的价值,第三行中有n个正整数,表示物品的重量。
输出:
将计算的装入背包物品的最大价值和最优装入方案输出到文件output.txt
输入样例:
input.txt:
5 10
6 3 5 4 6
2 2 6 5 4
输出样例:
output.txt
15
1 1 0 0 1
思路:设n个物品,价值和重量分别为v[i],w[i],背包承重为V,设f[i][j]为从前i个物品中选且重量不超过j的最大价值,可以得到两种情况: 对于第i件物品,若不下,f[i][j]=f[i-1][j]相当于不选 装得下,(j>=w[i]) 则考虑从前i-1,容量不超过j-w[i]中尝试装下,并与不选该物品的总价值比较,得到最大值 则状态转移方程为: f[i][j]=0 当i=0||j=0 f[i][j]=f[i-1][j] 当j<w[i] f[i][j]=max(f[i-1][j-w[i]]+v[i],f[i-1][j]); 因为第i物品选不选只需要看f[i][j]是否=f[i-1][j],因此求最优解只需要从表格最下方走到最上方 |
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N][N];
int v[N];//价值
int w[N];//重量
int choose[N];
void solve(int n,int V){
for(int i=n;i>1;i--){
if(f[i][V]==f[i-1][V]) choose[i]=0;
else {
choose[i]=1;
V-=w[i];
}
}
choose[1]=(f[1][V]?1:0);
}
void fun(int value[],int weight[],int n,int V){
for(int i=1;i<=n;i++){
for(int j=0;j<=V;j++){
if(j>=w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
}
}
cout<<f[n][V]<<endl;
solve(n,V);
for(int i=1;i<=n;i++)
cout<<choose[i]<<' ';
}
int main(){
int n,V;
cin>>n>>V;
for(int i=1;i<=n;i++){
cin>>v[i];
}
for(int i=1;i<=n;i++){
cin>>w[i];
}
fun(v,w,n,V);
}
(5)最长上升子序列
求一个字符串的最长递增子序列的长度。请分别给出最长递增子序列长度的O(n2)及O(nlogn)算法。
如:dabdbf最长递增子序列就是abdf,长度为4
输入
第一行一个整数0<n<20,表示有n个字符串要处理
随后的n行,每行有一个字符串,该字符串的长度不会超过10000
输出
输出字符串的最长递增子序列的长度
样例输入
3
aaa
ababc
abklmncdefg
样例输出
1
3
7
思路:(1)O(n²) 设序列A,f[i]为以i结尾的最长上升子序列的长度,f[i]最小为1,(即序列只有自己)那么f[i]可以由f[j](j<i)得到,即当a[i]>a[j]时,f[i]=f[j]+1,对于每个I,遍历所有j,得到最大值记为f[i],最后遍历所有f[i]得到最大值,复杂度o(n^2). 状态转移方程: f[i]=1 f[i]=f[j]+1 j<i and a[j]<a[i] |
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
using namespace std;
const int N=10010;
int f[N];
int fun(string s ){
for(int i=0;i<s.length();i++){ //以i结尾
f[i]=1;
for(int j=0;j<i;j++){
if(s[j]<s[i]) f[i]=max(f[i],f[j]+1);
}
}
int res=0;
for(int i=0;i<s.length();i++) res=max(res,f[i]);
return res;
}
int main(){
int n;
cin>>n;
while(n--){
string a;
cin>>a;
getchar();
cout<<fun(a)<<endl;
}
}
(2)O(nlogn) 思路:采用贪心的思想,对于相等长度的上升子序列的最后一个数越小越好,后面的数越有机会加入该序列,因此用单调队列f来存上升子序列,f[i]表示长度为i的上升子序列的末位的最小值。Idx是队尾指针,在遍历数组时,若a[i]比f[idx]大,则直接加入队列,若比队尾元素小,就在队列中找比他大的第一个数f[tmp]并将其替代(即更新长度为tmp的序列的末位更小),队列长度就是lis的最大长度 |
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10010;
int a[N];
int f[N]; //fi表示长度为i的上升序列的最有一个元素,显然相同长度最后一位越小越好
int find(int x,int ri){ //在单调队列中找大于等于x的最小的位置
int l=1,r=ri;
while(l<r){
int mid=l+r>>1;
if(f[mid]>=x) r=mid;
else l=mid+1;
}
return l;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int idx=0; //单调队列的指针下标
f[++idx]=a[1];
for(int i=2;i<=n;i++){
if(a[i]>f[idx]) f[++idx]=a[i];
else{
int tmp=find(a[i],idx);
f[tmp]=a[i];
}
}
cout<<idx;
}