动态规划模板总结

动态规划:

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

试用情况:

  1. 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
  2. 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
  3. 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

具体还是要根据具体问题分析

一,01背包

有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

关键是找出状态方程组,可知为dp[i][j]=max(dp[i-1][ j ],dp[i-1][j-w[i] ]+c[i]),所以可以写出代码

#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int dp[3405][13000],c[3405],w[3405];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&w[i],&c[i]);
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            if(j<w[i])
                dp[i][j]=dp[i-1][j];
            else
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

然而可不可以优化一下呢,答案是可以的,可以考虑将其换成一维数组,即

dp[ j ]=max(dp[ j ],dp[ j-w[i] ]+c[i] );

#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int dp[13000],c[3405],w[3405];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&w[i],&c[i]);
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;++i)
    {
        for(int j=m;j>=w[i];--j)
        {
                dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

这样就可以简化算法了。

二:最长公共子序列

给定两个字符串,寻找这两个字串之间的最长公共子序列。

可知其状态方程为

#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<math.h>
using namespace std;
int dp[1001][1001];
int main()
{
    string a,b;
    cin>>a>>b;
    memset(dp,0,sizeof(dp));   //初始化0
    for(int i=1;i<=a.length();++i)
    {
        for(int j=1;j<=b.length();++j)
        {
            if(a[i-1]==b[j-1])
                dp[i][j]=dp[i-1][j-1]+1;
            else
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
        }
    }
    cout<<dp[a.length()][b.length()];
    return 0;
}

但此方程无法求出序列,需要另设一个数组c[ i ][ j ],这样就可以记录dp数组的值来源,然后就可以回溯找到序列

#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
#include<math.h>
using namespace std;
const int maxn=1002;
int dp[maxn][maxn],c[maxn][maxn];
string a,b;
void LCS( )
{
    for(int i=1;i<=a.length();++i)
    {
        for(int j=1;j<=b.length();++j)
        {
            if(a[i-1]==b[j-1])
            {
                dp[i][j]=dp[i-1][j-1]+1;
                c[i][j]=1;
            }
            else if(dp[i][j-1]>=dp[i-1][j])
            {
                dp[i][j]=dp[i][j-1];
                c[i][j]=2;
            }
            else
            {
                dp[i][j]=dp[i-1][j];
                c[i][j]=3;
            }
        }
    }
}
void print(int i,int j)
{
    if(i==0 || j==0)
        return;
    if(c[i][j]==1)
    {
        print(i-1,j-1);
        cout<<a[i-1];
    }
    else if(c[i][j]==2)
        print(i,j-1);
    else
        print(i-1,j);
}
int main()
{
    cin>>a>>b;
    memset(dp,0,sizeof(dp));   //初始化0
    LCS();
    cout<<dp[a.length()][b.length()]<<endl;
    print(a.length(),b.length());
    return 0;
}

三:最长递增子序列

介绍两种方法,

第一种:最长公共子序列法:先将原数组排序,然后将排序后的数组与原来数组比较最长公共子序列

#include<iostream>
#include<stdio.h>
#include<cmath> 
#include<string>
#include<string.h>
#include<set>
#include<map>
#include <algorithm>
using namespace std;
/*方法1:将这n个数的序列排序之后,将最长递增子序列转变为LCS*/
int main() {
	int n;
	int A[100], B[100], res[100], len[105][105];
	while (scanf("%d", &n) == 1) {
		memset(res, 0, sizeof(res));
		memset(len, 0, sizeof(len));
		for (int i = 0; i < n; i++) {
			scanf("%d", &A[i]);
			B[i] = A[i];
		}
		sort(B, B + n);
		int i, j, cnt = 0;
		for (i = 1; i <= n; i++) {
			for (j = 1; j <= n; j++) {
				if(A[i-1] == B[j-1]) len[i][j] = 1 + len[i-1][j-1];
				else len[i][j] = max(len[i-1][j], len[i][j-1]);
			}
		}
		//输出任意一个最长公共子序列,倒叙遍历len数组
		 for (i = n, j = n; i > 0 && j > 0;) {
		 	if (len[i][j] == len[i-1][j]) {
		 		i--;
		 	}
		 	else if(len[i][j] == len[i][j-1]) {
		 		j--;
		 	}
		 	else {
		 		res[cnt++] = A[i-1];
		 		i--;
		 		j--;
		 	}
		 }
		 printf("%d\n%d", cnt, res[cnt-1]);//输出这个最长公共子序列。
		 for (i = cnt - 2; i >= 0; i--) printf(" %d", res[i]);
		 printf("\n");
	} 
	return 0;
}

第二种就是dp了,复杂度O(n^2),以dp[i]表示以i位结尾的最长递增子序列的长度。那么dp[i] = max(dp[i], dp[j]+1), j = 1, 2, 3,...,i-1。对于每个j<i遍历求出最大的dp[i],并用res[i] = j 来记录以i位结尾的最长递增子序列的前一位是谁,方便之后遍历输出子序列。

/*    输入数组,然后求最长递增子序列          */
#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
#include<math.h>
using namespace std;
const int maxn=1002;
int dp[maxn];
int LIS(int arr[],int len)
{
    for(int i=0;i<len;++i)
        dp[i]=1;
    for(int i=1;i<len;++i)
    {
        for(int j=0;j<i;++j)
        {
            if(arr[i]>arr[j] && dp[i]<dp[j]+1)
                dp[i]=dp[j]+1;
        }
    }
    int maxx=0;
    for(int i=0;i<len;++i)
        if(maxx<dp[i])
            maxx=dp[i];
    return maxx;
}
int main()
{
    int n,arr[maxn];
    scanf("%d",&n);
    for(int i=0;i<n;++i)
        scanf("%d",&arr[i]);
    int m=LIS(arr,n);
    printf("%d\n",m);
    return 0;
}

第三种就是利用二分的O(nlogn)的算法,但其只能求长度

int getLISLength(int num[], int length) {
 vector<int> ivec;
 for (int i = 0; i < length; ++i) {
     if (ivec.size() == 0 || ivec.back() < num[i])
         ivec.push_back(num[i]);
     else {
         int low = 0, high = ivec.size() - 1;
         while (low < high) {
             int mid = (low + high) / 2;
             if (ivec[mid] < num[i])
                 low = mid + 1;
             else
                 high = mid - 1;
         }
         ivec[low] =  num[i];
     }
 }
 return ivec.size();
}

四:最大子序列和

问题描述,给定一个连续序列,如{1,5,-2,9,7},让你求最大子序列和。

第一种方法,分治法,复杂度O(nlogn)

int maxsequence2(int a[], int l, int u)
{
    if (l > u) return 0;
    if (l == u) return a[l];
    int m = (l + u) / 2;
 
    /*求横跨左右的最大连续子序列左半部分*/   
    int lmax=a[m], lsum=0;
    for (int i=m; i>=l; i--) {
        lsum += a[i];
        if (lsum > lmax)
            lmax = lsum;
    }
    
    /*求横跨左右的最大连续子序列右半部分*/   
    int rmax=a[m+1], rsum = 0; 
    for (int i=m+1; i<=u; i++) { 
        rsum += a[i];
        if (rsum > rmax) 
            rmax = rsum; 
    }
    return max3(lmax+rmax, maxsequence2(a, l, m), maxsequence2(a, m+1, u)); //返回三者最大值
}
 
/*求三个数最大值*/
int max3(int i, int j, int k)
{
    if (i>=j && i>=k)
        return i;
    return max3(j, k, i);
} 

第二种是线性O(nlogn)

int maxsubseqsum(int a[],int n)
{
    int maxSum=0,thisSum=0;
    for(int i=0;i<n;++i)
    {
        thisSum+=a[i];
        if(thisSum>maxSum)
            maxSum=thisSum;
        else if(thisSum<0)
            thisSum=0;
    }
    return maxSum;
}

五:编辑距离

给定两个字符串,s1和s2,让你求通过插入删除修改等操作使两个字符串相等的最小次数(距离)

显然可以有如下动态规划公式:

  • if i == 0 且 j == 0,dp(i, j) = 0
  • if i == 0 且 j > 0,dp(i, j) = j
  • if i > 0 且j == 0,dp(i, j) = i
  • if i ≥ 1  且 j ≥ 1 ,dp(i, j) == min{dp(i-1, j) + 1, dp(i, j-1) + 1,dp(i-1, j-1) + temp },当第一个字符串的第i个字符不等于第二个字符串的第j个字符时,temp= 1;否则,temp = 0。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std ;
const int maxn=10005;
char str1[maxn],str2[maxn];
int dp[maxn][maxn];
int editdistance(char *str1,char *str2)
{
    int len1=strlen(str1);
    int len2=strlen(str2);
    for(int i=0;i<=len1;++i)
        dp[i][0]=i;       //第二个字符串长度为0,需要操作i次
    for(int j=0;j<=len2;++j)
        dp[0][j]=j;
    for(int i=1;i<=len1;++i)
    {
        for(int j=1;j<=len2;++j)
        {
            int temp;
            if(str1[i-1]==str2[j-1])
                temp=0;
            else
                temp=1;
            dp[i][j]=min(min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+temp);
        }              //求三个中最小的
    }
    return dp[len1][len2];
}
int main()
{
   cin>>str1>>str2;
   int t=editdistance(str1,str2);
   cout<<t;
    return 0 ;
}

六:最优三角剖分

给定凸多边形P,以及定义在由多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分,使得该三角剖分中诸三角形上权之和为最小。

状态方程为:其中w( v(i-1)v(k)v(j) )=g[i-1][k]+g[k][j]+g[i-1][j];

#include<iostream>
#include<sstream>
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std  ;
const int M= 1000 + 5 ;
int n ;
int s[M][M] ;    //记录路径
double m[M][M],g[M][M];   //记录最优解以及存储权值
void  Convexpolygontriangulation()
{
    for(int i = 1 ;i <= n ; i++)    // 初始化
    {
        m[i][i] = 0 ;
        s[i][i] = 0 ;
    }
    for(int d = 2 ;d <= n ; d++)         // 枚举点的个数
      for(int i = 1 ;i <= n - d + 1 ; i++)  // 枚举起始点
      {
          int j = i + d - 1 ;         // 终点
          m[i][j] = m[i+1][j] + g[i-1][i] + g[i][j] + g[i-1][j] ;
          s[i][j] = i ;
          for(int k = i + 1 ;k < j ; k++)     // 枚举中间点
          {
              double temp = m[i][k] + m[k+1][j] + g[i-1][k] + g[k][j] + g[i-1][j] ;
              if(m[i][j] > temp)
              {
                  m[i][j] = temp ;   // 更新最优值
                  s[i][j] = k ;      // 记录中间点
              }
          }
      }
}
void print(int i , int j)  // 输出所有的弦
{
    if(i == j)  return ;
    if(s[i][j]>i)
      cout<<"{v"<<i-1<<"v"<<s[i][j]<<"}"<<endl;
    if(j>s[i][j]+1)
      cout<<"{v"<<s[i][j]<<"v"<<j<<"}"<<endl;
    print(i ,s[i][j]);
    print(s[i][j]+1 ,j);
    //cout<<"{ v"<<i-1<<" , v"<<s[i][j]<<" , v"<<j<<" }"<<endl; //输出所有剖分后的三角形
}
int main()
{
    int i,j;
    cout << "请输入顶点的个数 n:";
    cin >> n;
    n-- ;
    cout << "请依次输入各顶点的连接权值:";
    for(int i = 0 ;i <= n ; i++)   // 输入各个顶点之间的距离
        for(int j = 0 ;j <= n ; j++)
            cin>>g[i][j] ;
    Convexpolygontriangulation();
    cout<<m[1][n]<<endl;
    print(1 ,n); // 打印路径
    return 0 ;
}

待更新... ...

猜你喜欢

转载自blog.csdn.net/qq_41785863/article/details/81457126