zoukankan      html  css  js  c++  java
  • (基础)--- 动态规划 --- 线性、背包、区间

    一、斐波那契

    递归

    1. 代码虽然简洁、但是低效。时间复杂度为O(2^n)
    2. 递归算法时间复杂度计算:子问题个数乘以解决一个子问题需要的时间
    int fib(int N) {
          if (N == 1 || N == 2) return 1;
          return fib(N - 1) + fib(N - 2);
    }
    

    观察递归树,存在大量的重复计算,导致低效

    记忆化搜索

    用数组等将已经算过的东西记录下来,在下一次要使用的时候,直接用已经计算过的值,避免重复计算,去掉重复的搜索树

    递推

    f[0] = f[1] = 1;
    for(int i = 2; i <= n; i ++ ){
        f[i] = f[i - 1] + f[i - 2];
    }
    return f[n];
    

    相关例题

    三步问题

    类似于斐波那契数列,采用递推的方式,记得加上long long,防止爆int.
    假设我们在第i阶阶梯上,上一步在i-1阶或者i-2阶或者i-3阶,根据分类加法原理
    f[i] = (f[i - 3] + f[i - 2] + f[i - 1])
    求到第1、2、3阶梯的时候,显然f[1] = 1,f[2] = 2,f[3] = 4

    class Solution {
    public:
        const int mod = 1000000007;
        long long f[1000010];
        int waysToStep(int n) {
           f[1] = 1,f[2] = 2,f[3] = 4;
           for(int i = 4; i <= n; i ++ ){
               f[i] = (f[i - 3] + f[i - 2] + f[i - 1]) % mod;
           }
           return f[n];
        }
    };
    

    动态规划原理——加法原理

    分类加法原理

    做一件事,完成它可以有n类方法,在第一类方法中有m1种不同的方法,在第二类方法中有m2种不同的方法,……,在第n类方法中有mn种不同的方法,那么完成这件事共有N = m1 + m2 + m3 + ... + mn种不同方法。

    分步乘法原理

    做一件事,完成它需要分成n个步骤,做第一步有m1种不同的方法,做第二步有m2种不同的方法,……,做第n步有mn种不同的方法,那么完成这件事共有N = m1 * m2 * m3 * ... * mn种不同的方法。

    动态规划的若干个定义:

    • 动态规划是解决多阶段决策过程最优化问题的一种方法
    • 阶段:把问题分成几个相互联系的有顺序的几个环节,这些环节即称为阶段
    • 状态:某一阶段的出发位置称为状态,通常一个阶段包含若干状态
    • 决策:从某阶段的一个状态演变到下一个阶段某状态的选择
    • 策略:从开始到终点的全过程,由每段决策组成的决策序列称为全过程策略,简称策略
    • 状态转移方程:由前一阶段到后一阶段演变规律,i阶段到i + 1阶段状态

    动态规划适用的基本条件

    具有相同子问题

    1. 首先,我们必须保证这个问题能够分解出几个子问题,并且能够通过这些子问题解决
    2. 其次,将这些子问题作为一个新问题,它也能分解成为相同的子问题进行描述

    满足最优子结构

    1. 问题的最优解包含着它的子问题的最优解。不管前面的策略如何,此后的决策必须是基于当前状态(由上一次决策产生)的最优决策

    满足无后效性

    1. 明动态规划只适用于解决当前决策与过去状态无关的问题。状态,出现在策略任何一个位置,它的地位相同,都可实施同样策略,这就是无后效性的内涵.
    2. 如果当前问题的具体决策,会对解决其它未来的问题产生影响,如果产生影响,就无法保证决策的最优性。

    动态规划分析步骤

    1. 结合原问题和子问题确定状态
      • 题目在求什么?要求出这个值我们需要知道什么?什么是影响答案的因素?
      • (一维描述不完就二维,二维不行就三维四维。)
      • 状态的参数一般有
      • 1)描述位置的:前(后)i单位,第i到第j单位,坐标为(i,j),第i个之前(后)且必须
      取第i个等
      • 2)描述数量的:取i个,不超过i个,至少i个等
      • 3)描述对后有影响的:状态压缩的,一些特殊的性质

    2. 确定转移方程
      • 1)检查参数是否足够;
      • 2)分情况:最后一次操作的方式,取不取,怎么样取——前一项是什么
      • 3)初始边界是什么。
      • 4)注意无后效性。比如说,求A就要求B,求B就要求C,而求C就要求A,这就
      不符合无后效性了。
      根据状态枚举最后一次决策(即当前状态怎么来的)就可确定出状态转移方程!

    3.考虑是否需要优化

    4.确定编程实现方式
    • 1)递推
    • 2)记忆化搜索

    例题:

    练习一:传球游戏

    代码如下:

    #include<iostream>
    
    using namespace std;
    
    const int N = 100;
    
    /*
    有多少种不同的传球方法
    可以使得从小蛮手里开始传的球,
    传了m次以后,又回到小蛮手里。
    
    原问题:第1号传m次到第1号 
    子问题:第1号传i次到第j号手里面 
    
    状态转移方程:f[i][j] = f[i - 1][j + 1] + f[i - 1][j - 1];
    初始化:f[0][1] = 1 
    环:j = 1 , j - 1 = n
    	j = n,  j + 1 = 1 
    
    */
    
    int n,m;
    int f[N][N];
    
    int main() {
    	cin >> n >> m;
    	f[0][1] = 1;
    	for(int i = 1; i <= m; i ++ ){
    		for(int j = 1; j <= n; j ++ ){
    			if(j == 1) f[i][j] = f[i - 1][j + 1] + f[i - 1][n];
    			else if(j == n) f[i][j] = f[i - 1][1] + f[i - 1][j - 1];
    			else f[i][j] = f[i - 1][j + 1] + f[i - 1][j - 1];
    		}
    	}
    	cout<<f[m][1];
    	return 0;
    }
    

    [练习二:最长上升子序列(LIS)](https://leetcode-cn.com/problems/longest-increasing-subsequence/submissions/)

    代码如下:

    #include<iostream>
    
    using namespace std;
    
    const int N = 1010;
    
    /*
    原问题:整个数列(1-N)的单调递增的子序列(不连续)最长的长度
    子问题:以i结尾的单调递增的最长长度 
    
    转移方程:f[i] = max(f[j] + 1,f[i])(a[j] < a[i], j < i)
    
    初始化:f[1 ~ n] = 1 (每个a[i]自身长度为1) 
     
    样例:
    5
    4 2 3 1 5
    输出:3 
    
    */
    int n;
    int a[N];
    int f[N];
    
    int main() {
    	cin >> n;
    	for(int i = 1; i <= n; i ++ ){
    		cin >> a[i]; 
    	}
    	int res = 0;
    	for(int i = 1; i <= n; i ++ ){
    		f[i] = 1; 
    		for(int j = 1; j < i; j ++ ){
    			if(a[j] < a[i]){
    				f[i] = max(f[j] + 1,f[i]);
    			}
    		}
    		res = max(f[i],res);
    	}
    	cout<<res;
    	return 0;
    }
    

    [练习三:滑雪](https://ac.nowcoder.com/acm/problem/105685)

    代码如下:

    1. 动态规划做法,记忆化搜索
    #include<iostream>
    
    using namespace std;
    
    const int N = 110;
    
    /*
    
    原问题:从(1,1)~(n,m)的任意一点下滑最长的长度
    子问题:从(i,j)下滑最长的长度
    
    状态转移:从某个点滑向上下左右相邻四个点之一
    
    转移方程: f[i][j] = f[i - 1][j] + 1 (a[i - 1][j] < a[i][j])
    		  f[i][j] = f[i + 1][j] + 1 (a[i + 1][j] < a[i][j])
    		  f[i][j] = f[i][j - 1] + 1 (a[i][j - 1] < a[i][j])
    		  f[i][j] = f[i][j + 1] + 1 (a[i][j + 1] < a[i][j])
    
    初始化:f[i][j] = 1;
    
    */
    
    int n,m;
    int a[N][N],f[N][N];
    
    int find(int i,int j) {
    	if(f[i][j]) return f[i][j];
    	f[i][j] = 1;
    	if(i > 1 && a[i - 1][j] < a[i][j])
    		f[i][j] = max(find(i - 1,j) + 1,f[i][j]);
    	if(i < n && a[i + 1][j] < a[i][j])
    		f[i][j] = max(find(i + 1,j) + 1,f[i][j]);
    	if(j > 1 && a[i][j - 1] < a[i][j])
    		f[i][j] = max(find(i,j - 1) + 1,f[i][j]);
    	if(j < m && a[i][j + 1] < a[i][j])
    		f[i][j] = max(find(i,j + 1) + 1,f[i][j]);
    	return f[i][j];
    }
    int main() {
    	cin >> n >> m;
    	for(int i = 1; i <= n; i ++ ) {
    		for(int j = 1; j <= m; j ++ ) {
    			cin >> a[i][j];
    		}
    	}
    	int res = 0;
    	for(int i = 1; i <= n; i ++ ) {
    		for(int j = 1; j <= m; j ++ ) {
    			res = max(res,find(i,j));
    		}
    	}
    	cout<<res;
    	return 0;
    }
    
    1. DFS搜索,用st数组标记并且记录最长路径
    #include<iostream>
    
    using namespace std;
    
    const int N = 110;
    
    /*
    
    */
    
    int n,m;
    int g[N][N];
    int st[N][N]; //用st数组记录从此点出发的最长路径长度 
    int dx[4] = {0,1,0,-1};
    int dy[4] = {1,0,-1,0};
    
    int ans;
    
    int dfs(int x,int y){
    	if(st[x][y]) return st[x][y];
    	//初始路径长度为1 
    	int u = 1;
    	//上下左右 
    	for(int i = 0; i < 4; i ++ ){
    		int a = x + dx[i];
    		int b = y + dy[i];
    		if(a < 1 || a > n || b < 1 || b > m || g[a][b] >= g[x][y]) continue;
    		u = max(u,dfs(a,b) + 1);
    	}
    	//记录从(x,y)出发的最长路径长度 
    	st[x][y] = u;
    	//更新路径长度 
    	ans = max(ans,u);
    	return u;
    }
    int main() {
    	cin >> n >> m;
    	for(int i = 1; i <= n; i ++ ){
    		for(int j = 1; j <= m; j ++ ){
    			cin >> g[i][j];
    		}
    	}	
    	for(int i = 1; i <= n; i ++ ){
    		for(int j = 1; j <= m; j ++ ){
    			dfs(i,j);
    		}
    	}
    	cout<<ans;
    	return 0;
    }
    

    练习四:最大子串和

    给你一个有正有负的序列,求一个子串(连续的一段),使其和最大!

    代码如下:

    #include<iostream>
    
    using namespace std;
    
    const int N = 110;
    
    /*
    原问题:求(1 ~ n)中最大字串和 
    子问题:前i个数的最大子串和 
    
    转移方程:f[i] = f[i - 1] + a[i];
    
    初始化为:f[i] = a[i];
     
    输入: 
    6 
    -5 6 -1 5 4 -7
    输出:
    14
     
    */
    int n,a[N],f[N];
    
    int main() {
    	cin >> n;
    	for(int i = 1; i <= n; i ++ ) cin >> a[i];
    	int res = -1e9;
    	for(int i = 1; i <= n; i ++ ){
    		f[i] = a[i];
    		f[i] = max(f[i],f[i - 1] + a[i]);
    		res = max(res,f[i]);
    	}
    	cout<<res;		
    	return 0;
    }
    

    练习五:最长公共子序列(LCS)

    代码如下:
    1.非接口

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    
    using namespace std;
    
    const int N = 1010;
    
    /*
    原问题:两个字符串中最长公共子序列 
    
    状态表示:f[i][j]表示前一个字符串的第i位与后一个字符串的前j位的最长公共子序列长度 
    
    转移方程:
    如果s1[i] == s2[j] :f[i][j] = f[i - 1][j - 1] + 1; 
    	s1[i] != s2[j] : f[i][j] = max(f[i - 1][j],f[i][j - 1]);
    
    输入: 
    4 4
    abcd becd
    
    输出:
    3("bcd") 
    */
    int n,m;
    char s1[N],s2[N];
    int f[N][N];
    
    int main() {
    	cin >> n >> m;
    	scanf("%s %s",s1 + 1,s2 + 1);		
    	for(int i = 1; i <= n; i ++ ){
    		for(int j = 1; j <= m; j ++ ){
    			if(s1[i] == s2[j]){
    				f[i][j] = f[i - 1][j - 1] + 1;
    			}else{
    				f[i][j] = max(f[i - 1][j],f[i][j - 1]);
    			}
    		}
    	}
    	cout<<f[n][m];
    	return 0;
    }
    

    2.接口

    class Solution {
    public:
        int f[1010][1010];
        int longestCommonSubsequence(string s1, string s2) {
            int n = s1.size(),m = s2.size();
            for(int i = 1; i <= n; i ++ ){
                for(int j = 1; j <= m; j ++ ){
                    if(s1[i - 1] == s2[j - 1]){
                        f[i][j] = f[i - 1][j - 1] + 1;
                    }else{
                        f[i][j] = max(f[i - 1][j],f[i][j - 1]);
                    }
                }
            }
            return f[n][m];
        }
    };
    
  • 相关阅读:
    艾伟_转载:你知道吗?——ASP.NET的Session会导致的性能问题 狼人:
    艾伟_转载:一次挂死(hang)的处理过程及经验 狼人:
    艾伟也谈项目管理,微型项目实践感悟 狼人:
    艾伟_转载:[原创]再谈IIS与ASP.NET管道 狼人:
    艾伟_转载:企业库缓存依赖的实现基于文件依赖 狼人:
    艾伟也谈项目管理,我也发软件开发团队的思考(侧重点是人员) 狼人:
    MYSQL用户名:root
    map 和 unordered_map以char * 为key
    设计模式单例模式(singleton)
    Android允许其他应用程序启动你的Activity
  • 原文地址:https://www.cnblogs.com/bingers/p/13863345.html
Copyright © 2011-2022 走看看