动态规划从入门到精通

目录

动态规划的详解

动态规划的应用

     机器人到达指定位置数

     换钱的最少货币数

     排成一条线的纸牌博弈问题

     象棋中马的跳法

     Bob的生存概率 

     换钱的方法数 

动态规划的总结

动态规划的详解

       暴力尝试递归操作中有很多重复计算的操作,浪费时间。动态规划就是减少暴力尝试中重复计算的技巧,这种技巧就是一个大型套路,先写出用尝试的思路解决问题的递归函数,而不用操心时间复杂度,这个过程是无可替代的,没有套路的,只能依靠个人智慧或者足够多的经验。

       但是怎么把尝试的版本,优化成动态规划,是有固定套路的,大体步骤如下:

       (1)找到什么可变参数可以代表一个递归状态,也就是哪些参数一旦确定,返回值就确定了;

       (2)把可变参数的所有组合映射成一张表,有 1 个可变参数就是一维表,2 个可变参数就是二维表,......

       (3)最终答案要的是表中的哪个位置,在表中标出;

       (4)根据递归过程的 base case,把这张表的最简单、不需要依赖其他位置的那些位置填好值;

       (5)根据递归过程非base case的部分,也就是分析表中的普遍位置需要怎么计算得到,那么这张表的填写顺序也就确定了;

       (6)填好表,返回最终答案在表中位置的值;

       对于代码方面的修改也是有固定套路的,对于记忆化搜索的方法就是首先写出尝试的思路解决问题的递归函数,然后在此基础上先改成记忆化搜索的程序,也就是添加上数组,记录计算过的值,避免出现重复计算的过程,后面执行程序时对于计算过的直接使用不再重复计算。

       严格位置表依赖的方法是将按照上面的步骤将目标值和初始确定的值在程序中先确定出来,然后对递归程序进行适当更改即可完成。

动态规划的应用

     机器人到达指定位置数

     【题目】 假设有排成一行的N个位置,记为1~N,N 一定大于或等于 2。开始时机器人在其中的 M 位置上(M一定是 1~N 中的一个),机器人可以往左走或者往右走,如果机器人来到1位置,那么下一步只能往右来到2 位置;如果机器人来到N位置,那么下一步只能往左来到 N-1 位置。规定机器人必须走K步,最终能来到P位置(P 也一定是 1~N 中的一个)的方法有多少种。给定四个参数 N、M、K、P,返回方法数。

     【举例】 N=5,M=2,K=3,P=3 上面的参数代表所有位置为1 2 3 4 5。机器人最开始在2位置上,必须经过3步,最后到达3位置。走的方法只有如下3种: (1)从2到1,从1到2,从2到3 (2)从2到3,从3到2,从2到3 (3)从2到3,从3到4,从4到3。所以返回方法数3。 N=3,M=1,K=3,P=3 上面的参数代表所有位置为1 2 3。机器人最开始在1位置上,必须经过3步,最后到达3位置。怎么走也不可能,所以返回方法数0。

    public static int ways1(int N, int M, int K, int P) {//使用暴力递归的方式解决问题,时间复杂度能达到O(2^k)
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		// 总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
		return walk(N, M, K, P);
	}

	// N : 位置为1 ~ N,固定参数
	// cur : 当前在cur位置,可变参数
	// rest : 还剩res步没有走,可变参数
	// P : 最终目标位置是P,固定参数
	// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回
	public static int walk(int N, int cur, int rest, int P) {
		// 如果没有剩余步数了,当前的cur位置就是最后的位置
		// 如果最后的位置停在P上,那么之前做的移动是有效的
		// 如果最后的位置没在P上,那么之前做的移动是无效的
		if (rest == 0) {
			return cur == P ? 1 : 0;
		}
		// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2
		// 后续的过程就是,来到2位置上,还剩rest-1步要走
		if (cur == 1) {
			return walk(N, 2, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1
		// 后续的过程就是,来到N-1位置上,还剩rest-1步要走
		if (cur == N) {
			return walk(N, N - 1, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右
		// 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走
		// 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走
		// 走向左、走向右是截然不同的方法,所以总方法数要都算上
		return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
	}
   public static int ways2(int N, int M, int K, int P) {//使用记忆化搜索的方式解决问题,时间复杂度为O(M*K)
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
        int[][] dp = new int[K + 1][N + 1];//定义一个数组存放计算过的内容,作为缓存结构
        for(int i=0;i<=k;i++){
            for(int j=0;j<=N;j++){
                dp[i][j]= -1;
            }
        }
		// 总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
		return walk2(N, M, K, P , dp);
	}

	// N : 位置为1 ~ N,固定参数
	// cur : 当前在cur位置,可变参数
	// rest : 还剩res步没有走,可变参数
	// P : 最终目标位置是P,固定参数
	// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回
	public static int walk2(int N, int cur, int rest, int P,int[][] dp) {
	    if(dp[rest][cur]!=-1){//如果某个值已经计算过,不需要再重复计算,直接返回
              return dp[rest][cur];
        }
         //还没有计算过,每次返回之前把答案记录下来
		if (rest == 0) {
            dp[rest][cur] = cur == P ? 1 : 0;
			return dp[rest][cur];
		}
		// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2
		// 后续的过程就是,来到2位置上,还剩rest-1步要走
		if (cur == 1) {
            dp[rest][cur] =walk2(N, 2, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1
		// 后续的过程就是,来到N-1位置上,还剩rest-1步要走
		else if (cur == N) {
            dp[rest][cur] =  walk2(N, N - 1, rest - 1, P);
		}
        else{
         dp[rest][cur] = walk2(N, cur + 1, rest - 1, P) + walk2(N, cur - 1, rest - 1, P);
        }
		return dp[rest][cur];
	}


	public static int ways3(int N, int M, int K, int P) {//严格位置表依赖的方式
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		int[][] dp = new int[K + 1][N + 1];//定义一个数组存放计算过的内容
		dp[0][P] = 1;//终点的位置在格子中标出来
		for (int i = 1; i <= K; i++) {//然后从第一行第一列开始,下面的过程根据递归的依赖性,进行改编
			for (int j = 1; j <= N; j++) {
				if (j == 1) {
					dp[i][j] = dp[i - 1][2];
				} else if (j == N) {
					dp[i][j] = dp[i - 1][N - 1];
				} else {
					dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
				}
			}
		}
		return dp[K][M];
	}

     换钱的最少货币数

     【题目】给定数组 arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数 aim,代表要找的钱数,求组成aim的最少货币数。

     【举例】arr=[5,2,3],aim=20。4 张 5 元可以组成 20 元,其他的找钱方案都要使用更多张的货币,所以返回 4。arr=[5,2,3],aim=0。不用任何货币就可以组成0元,返回 0。arr=[3,5],aim=2。根本无法组成2元,钱不能找开的情况下默认返回-1。

    public static int minCoins1(int[] arr, int aim) {//暴力递归方式求解
		if (arr == null || arr.length == 0 || aim < 0) {
			return -1;
		}
		return process(arr, 0, aim);
	}
	// 当前考虑的面值是arr[i],还剩rest的钱需要找零
	// 如果返回-1说明自由使用arr[i..N-1]面值的情况下,无论如何也无法找零rest
	// 如果返回不是-1,代表自由使用arr[i..N-1]面值的情况下,找零rest需要的最少张数
	public static int process(int[] arr, int i, int rest) {
	    if(rest < 0){
            return -1;
        }
        if(rest == 0){
            return 0;
        }
        //rest>0但是没有钱
        if(i == arr.length){
            return -1;
        }
        //rest > 0并且也有硬币,有两种选择
        int p1 = process(arr,i+1,rest);//表示不要下一个硬币
        int p2Next = process(arr,i+1,rest-arr[i]);//表示要下一个硬币
        if(p1 == -1&&p2Next == -1){
             return -1;
        }
        else{
             if(p1 = -1){
                 return p2Next +1;//加1是因为p2Next表示要下一个硬币
             }
             if(p2 = -1){
                 return p1;
             }
             return Math.min(p1,p2Next+1);
        }
    }
     public static int minCoins2(int[] arr, int aim) {//记忆化搜索的动态规划的方式求解
		if (arr == null || arr.length == 0 || aim < 0) {
			return -1;
		}
        int[][] dp = new int[arr.length+1][aim+1];//建立数组记录计算的过程
        for(int i = 0;i <= arr.length; i++){//初始化
            for(int j = 0;j <= aim; j++){
                 dp[i][j] = -2;
            }
         }
		return process2(arr, 0, aim,dp);

	}
	// 当前考虑的面值是arr[i],还剩rest的钱需要找零
	// 如果返回-1说明自由使用arr[i..N-1]面值的情况下,无论如何也无法找零rest
	// 如果返回不是-1,代表自由使用arr[i..N-1]面值的情况下,找零rest需要的最少张数
	public static int process2(int[] arr, int i, int rest , int[][] dp) {
        if(rest < 0){
            return -1;
        }
        if(dp[i][rest]!=-2){//如果已经计算过,直接返回
            return dp[i][rest];
        }
        if(rest == 0){
            dp[i][rest] = 0;
        }
        else if(i == arr.length){
            dp[i][rest] = -1;
        }else{
        //rest > 0并且也有硬币,有两种选择
        int p1 = process2(arr,i+1,rest,dp);//表示不要下一个硬币
        int p2Next = process2(arr,i+1,rest-arr[i],dp);//表示要下一个硬币
        if(p1 == -1&&p2Next == -1){
             dp[i][rest] = -1;
        }
        else{
             if(p1 = -1){
                 dp[i][rest] = p2Next +1;//加1是因为p2Next表示要下一个硬币
             }
             else if(p2 = -1){
                dp[i][rest] = p1;
             }else{
                 dp[i][rest] = Math.min(p1,p2Next+1);
              }
           }
        }
        return dp[i][rest];
    }

	public static int minCoins3(int[] arr, int aim) {//严格表结构的动态规划方式求解
		if (arr == null || arr.length == 0 || aim < 0) {
			return -1;
		}
		int N = arr.length;
		int[][] dp = new int[N + 1][aim + 1];
		// 设置最后一排的值,除了dp[N][0]为0之外,其他都是-1
        //一些知道的初始位置设置好
		for (int col = 1; col <= aim; col++) {
			dp[N][col] = -1;
		}
        for(int row = 0;row <= N;row++){
            dp[row][0] = 0;
        }
        //把递归的过程放过来,然后根据表结构进行适当的改动
		for(int i = N-1; i>= 0;i--){
            for(int rest = 1;rest <= aim;rest++){
                   int p1 = dp[i+1][rest];
                   int p2Next = -1;
                   if(rest - arr[i] >= 0){
                       p2Next = dp[i+1][rest - arr[i]];
                   }
                     
                   if(p1 == -1&&p2Next == -1){
                       dp[i+1][rest] = -1;
                   }
                   else{
                       if(p1 = -1){
                          dp[i+1][rest] = p2Next +1;//加1是因为p2Next表示要下一个硬币
                        }
                        if(p2 = -1){
                          dp[i+1][rest] = p1;
                         }
                          dp[i+1][rest] = Math.min(p1,p2Next+1);
                   }
             }
        }
                 
		return dp[0][aim];
	}

     排成一条线的纸牌博弈问题

     【题目】给定一个整型数组 arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩 家B都绝顶聪明。请返回最后获胜者的分数。

     【举例】arr=[1,2,100,4]。开始时,玩家A只能拿走1或4。如果玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A。如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A。玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B 不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。arr=[1,100,2]。 开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为 100。所以返回100。

    public static int win1(int[] arr) {//暴力递归的方式求解
		if (arr == null || arr.length == 0) {
			return 0;
		}
		return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));//先手和后手谁的分数多,谁获胜
	}

	public static int f(int[] arr, int i, int j) {//先手函数
		if (i == j) {//如果只有一个数字,先手直接拿了
			return arr[i];
		}
		return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));//如果不是只有一个数字,那么先手选择拿左边和右边两种情况下,最大的那一种情况
	}

	public static int s(int[] arr, int i, int j) {//后手函数
		if (i == j) {//如果只有一张牌,后手拿不到
			return 0;
		}
		return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));//如果不只有一张牌,后手只能拿到剩下情况下最小的那种情况
	}
    //在范围上尝试的模型,行是不可能超过列的,左下角区域都是不存在的,先填对角线
    //动态规划一定要画图操作,用最基础的递归操作进行画图,找到格子之间的关系,然后递归的过程进行改写
    public static int win2(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
        //建立两个格子
		int[][] f = new int[arr.length][arr.length];
		int[][] s = new int[arr.length][arr.length];
		for (int j = 0; j < arr.length; j++) {
			f[j][j] = arr[j];//对角线元素填上
            s[j][j] = 0;
			for (int i = j - 1; i >= 0; i--) {//只对右上角进行操作,两个表互相依赖
				f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
				s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
			}
		}
		return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
	}


     象棋中马的跳法

     【题目】请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置。那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。给你三个 参数,x,y,k,返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数有多少种?

    public static int getWays(int x, int y, int step) {//暴力递归的方式求解
		return process(x, y, step);
	}

	public static int process(int x, int y, int step) {
		if (x < 0 || x > 8 || y < 0 || y > 9) {
			return 0;
		}//x,y位置越界,0种方法,无法到达
		if (step == 0) {//不能再动了
			return (x == 0 && y == 0) ? 1 : 0;//一开始在(0,0)位置,如果想要到达的就是(0,0)位置,那么已经到达,一种方法,如果不是那么无法到达
		}
        //不越界也可以跳,把跳一步可以跳到(x,y)位置的情况都写出来
		return process(x - 1, y + 2, step - 1)
				+ process(x + 1, y + 2, step - 1)
				+ process(x + 2, y + 1, step - 1)
				+ process(x + 2, y - 1, step - 1)
				+ process(x + 1, y - 2, step - 1)
				+ process(x - 1, y - 2, step - 1)
				+ process(x - 2, y - 1, step - 1)
				+ process(x - 2, y + 1, step - 1);
	}

	public static int dpWays(int x, int y, int step) {//严格表结构的动态规划方式求解
        //有三个可变参数,那么建立一个三维立体,其它的按照递归的程序和立体图形各个之间的关系进行改写
		if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
			return 0;
		}//这个立体之外的部分都是0
		int[][][] dp = new int[9][10][step + 1];//建立一个立体
		dp[0][0][0] = 1;//第0层的面只有(0,0)位置是1,其它都是0
		for (int h = 1; h <= step; h++) {//每一层处理,每一层只依赖于下一层的内容
			for (int r = 0; r < 9; r++) {
				for (int c = 0; c < 10; c++) {
					dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
				}
			}
		}
		return dp[x][y][step];
	}

	public static int getValue(int[][][] dp, int row, int col, int step) {//防止越界的函数,如果越界取0,如果没有越界,拿到相应位置的值
		if (row < 0 || row > 8 || col < 0 || col > 9) {
			return 0;
		}
		return dp[row][col][step];
	}

     Bob的生存概率 

     【题目】给定五个参数n,m,i,j,k。表示在一个N*M的区域,Bob处在(i,j)点,每次Bob等概率的向上、下、左、右四个方向移动一步,Bob必须走K步。如果走完之后,Bob还停留在这个区域上, 就算Bob存活,否则就算Bob死亡。请求解Bob的生存概率,返回字符串表示分数的方式。

   public static String bob1(int N, int M, int i, int j, int K) {//暴力递归的方式求解
		long all = (long) Math.pow(4, K);//总的方法数位4的k次方,因为每一个位置的选择有4种,一共走k步
		long live = process(N, M, i, j, K);
		long gcd = gcd(all, live);//概率就是活下来的除以总的
		return String.valueOf((live / gcd) + "/" + (all / gcd));
	}

	public static long process(int N, int M, int row, int col, int rest) {
		if (row < 0 || row == N || col < 0 || col == M) {
			return 0;
		}//如果越界,死亡
		if (rest == 0) {//如果已经走完也没有越界,活下来
			return 1;
		}
        //Bob总体活下来的方法数,等于他往上,往下,往左,往右分别走一步且活下来的方法数
		long live = process(N, M, row - 1, col, rest - 1);
		live += process(N, M, row + 1, col, rest - 1);
		live += process(N, M, row, col - 1, rest - 1);
		live += process(N, M, row, col + 1, rest - 1);
		return live;
	}

	public static long gcd(long m, long n) {//求最大公约数
		return n == 0 ? m : gcd(n, m % n);
	}

	public static String bob2(int N, int M, int i, int j, int K) {//严格表结构的动态规划的方式,同样的按照递归的方式,分析立体结构的关系求解
		int[][][] dp = new int[N + 2][M + 2][K + 1];//建立一个立体
		for (int row = 1; row <= N; row++) {
			for (int col = 1; col <= M; col++) {
				dp[row][col][0] = 1;
			}
		}
		for (int rest = 1; rest <= K; rest++) {
			for (int row = 1; row <= N; row++) {
				for (int col = 1; col <= M; col++) {
					dp[row][col][rest] = dp[row - 1][col][rest - 1];
					dp[row][col][rest] += dp[row + 1][col][rest - 1];
					dp[row][col][rest] += dp[row][col - 1][rest - 1];
					dp[row][col][rest] += dp[row][col + 1][rest - 1];
				}
			}
		}
		long all = (long) Math.pow(4, K);
		long live = dp[i + 1][j + 1][K];
		long gcd = gcd(all, live);
		return String.valueOf((live / gcd) + "/" + (all / gcd));
	}

     换钱的方法数 

       有给定面值的零钱数在arr数组中,最终需要找零的钱数为aim,返回最终能够找零的方法数。

public static int way1(int[] arr, int aim){//暴力递归方法的求解
      return process(arr,0,aim);//可以使用arr[0..]中的所有面值
}
//可以自由使用arr[index..]所有的面值
pubilc static int process(int[] arr,int index,int rest){
      if(index == arr.length){//如果已经没有钱数可以选择
         return rest == 0? 1:0;//那么如果不需要货币,只有一种方法,其它的返回0
      }
      int ways = 0;
      for(int zhang = 0; arr[index] * zhang <= rest; zhang ++){//只要选择的面值乘以张数不超过总计需要的,就可以随便选
          ways += process(arr,index + 1,rest - arr[index] * zhang);
      }
      return ways;
}
public static int ways2(int[] arr,int aim){//严格表结构的动态规划的方式求解,没有优化枚举结构,还是对递归方式进行适当的改动即可
     if(arr == null||arr.length == 0){
        return 0;
     }
     int N = arr.length;
     int[][] dp = new int[N+1][aim+1];
     dp[N][0] = 1;
     for(int index = N-1;index >= 0;index--){
         for(int rest = 0;rest <= aim;rest++){
             int ways = 0;
             for(int zhang = 0;arr[index] * zhang <= rest;zhang ++){
                  ways += dp[index+1][rest - arr[index] * zhang];
              }
              dp[index][rest] = ways;
         }
      }
      return dp[0][aim];
} 
public static int ways3(int[] arr,int aim){//严格表结构的动态规划的方式求解,优化枚举结构,其实也就是通过对格子中位置求解的观察,发现枚举行为和周围格子的关系,利用这个关系减少优化(称为斜率优化),对于同一行重复需要的内容,不再重新计算
     if(arr == null||arr.length == 0){
        return 0;
     }
     int N = arr.length;
     int[][] dp = new int[N+1][aim+1];
     dp[N][0] = 1;
     for(int index = N-1;index >= 0;index--){
         for(int rest = 0;rest <= aim;rest++){
             dp[index][rest] += dp[index][rest];//一个新的需要计算的格子,一定需要它下面的格子。
             if(rest - arr[index] >= 0){//如果还没有凑够
                 dp[index][rest] += dp[index][rest - arr[index]];//加上自己同行减去本行的面值位置的值
             }
        
         }
      }
      return dp[0][aim];
} 

动态规划的总结

       动态规划首先最重要的就是尝试,尝试的方式有从左到右以及范围尝试等比较重要的尝试方法,然后根据对题目的分析,写出暴力递归方式的代码,此时加上一个缓存数组,减少重复内容的重复计算,也就是改写成记忆化搜索的动态规划方式,此时并没有研究各个变量之间的依赖性,只是加了一个缓存结构。后面再根据这些关系,画出严格表结构,根据一些知道的内容,对表架构进行填充,表中需要求解的位置,根据记忆化搜索的代码和暴力递归的代码,分析出各个格子之间的关系,此时就可以根据暴力递归的代码该写出严格表结构的动态规划的代码,写出严格表结构进行分析能够对类似于枚举行为的结构进行优化,这是非常重要的。

       而尝试方法的好坏考虑的有两个方面,一是可变参数的个数,可变参数的个数越少,分析严格表结构时维度越低,更简单。二是单可变参数的维度,也就是一个参数的维度最好就是一个整数,这个是一定要保证的。

你可能感兴趣的:(数据结构与算法,动态规划,算法,数据结构,java)