【动态规划之斜率优化】

当填表时有枚举行为,就可能涉及到优化,通常就是斜率优化。

  1. 问题描述:给出5个参数,N,M,row,col,k,表示在N * M的区域上,李四初始在(row,col)位置,他一共要迈出k步,且每步都会等概率向上下左右四个方向走一个单位,任何时候李四只要离开N*M的区域,就直接死亡,走了k步之后,李四还在N * M区域的概率,即李四活着的概率。

分析:每一步都有种可能,走k步一共4k种可能,计算有多少种可能在区域内(注意当走出区域后直接死亡)

#include<iostream>
#include<cmath>
using namespace std;
//当前在(row,col),还有rest要走,返回还在范围内的可能数
int f(int N,int M,int row,int col,int rest) {
	if(row<0 ||row>=N ||col<0 || col>=M) { //越界了直接返回0种可能
		return 0;
	}
	if(rest==0) {
		return 1;//如果还在范围内,rest为0,返回1种可能
	}
	int up=f(N,M,row-1,col,rest-1);
	int down=f(N,M,row+1,col,rest-1);
	int left=f(N,M,row,col-1,rest-1);
	int right=f(N,M,row,col+1,rest-1);
	return up+down+left+right;

}
int main() {


	double result=(double)f(6,5,2,3,5)/pow(4,5);
	cout<<result;
	return 0;
}

  1. 问题描述:给出3个参数,N,M,K,怪兽有N滴血,等着英雄来砍自己,英雄每一次打击,都会让怪兽流失[0M]的血量,到底流失多少?每一次在[0M]上等概率的获取一个值,求K次打击之后,英雄把怪兽砍死的概率

分析:
样本对应模型
这也是一个概率的题目,同样要计算被砍死的情况数。
注意这道题和上一题的区别在于,由于要计算砍死的可能性,所以这道题只有k为0时才是终止情况,因此即使当前怪兽血量小于等于0l依然要继续砍,而上一题是计算存活的情况数,当走出区域后就直接死亡了,自然也不用计算了。

#include<iostream>
#include<cmath>
using namespace std;
//当前怪兽还有N滴血,还剩K刀
//在0-M范围上砍血
//返回当前被该死的情况数
int f(int N,int M,int K){
	if(K==0){//如果血量小于等于0,说明已经砍死。(注意如果当前怪兽已经是小于等于0了,但是K大于0,依然要继续砍,因为被砍到0了,之后的情况依然要算上
		return N<=0?1:0;
	}
	int ways=0;
	for(int i=0;i<=M;i++){
		ways+=f(N-i,M,K-1);
	}
	return ways;
}
int main(){
	int N;
	int M;
	int K;
	cin>>N>>M>>K;
	int sum=f(N,M,K);
	double result=(double)sum/(double)pow((M+1),K);
	cout<<result;
	return 0;
}

改进的暴力递归:
当前血量小于等于0时假设还剩K刀,死亡的可能性:(M+1)K

#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
//当前怪兽还有N滴血,还剩K刀
//在0-M范围上砍血
//返回当前被该死的情况数
int f(int N,int M,int K){
	if(K==0){//如果血量小于等于0,说明已经砍死。(注意如果当前怪兽已经是小于等于0了,但是K大于0,依然要继续砍,因为被砍到0了,之后的情况依然要算上
		return N<=0?1:0;
	}
	if(N<=0){	
		return pow(M+1,K);//死亡的情况数
	}
	int ways=0;
	for(int i=0;i<=M;i++){
		ways+=f(N-i,M,K-1);
	}
	return ways;
}
int main(){
	int N;
	int M;
	int K;
	cin>>N>>M>>K;
	int sum=f(N,M,K);
	double result=(double)sum/(double)pow((M+1),K);
	cout<<result;
	return 0;
}

严格表结构的动态规划初级版

#include<iostream>
#include<cmath>
#include<vector>
using namespace std;

//当前怪兽还有N滴血,还剩K刀
//在0-M范围上砍血
//返回当前被该死的情况数
int f(int N,int M,int K){
	if(K==0){//如果血量小于等于0,说明已经砍死。(注意如果当前怪兽已经是小于等于0了,但是K大于0,依然要继续砍,因为被砍到0了,之后的情况依然要算上
		return N<=0?1:0;
	}
	if(N<=0){	
		return pow(M+1,K);//死亡的情况数
	}
	int ways=0;
	for(int i=0;i<=M;i++){
		ways+=f(N-i,M,K-1);
	}
	return ways;
}
int main() {
	int N;
	int M;
	int K;
	cin>>N>>M>>K;
	vector<vector<int>>dp(K+1,vector<int>(N+1));
	dp[0][0]=1;
	for(int times=1; times<=K;times++) {
		dp[times][0]=pow(M+1,times);//第一列符合递归的终止条件,可以直接计算出来
		for(int j=1; j<=N; j++) {
			int ways=0;
			for(int i=0; i<=M; i++) {
				if(j-i<0){
					ways+=pow(M+1,times-1);//列越界的直接计算出来
				}
				
				else{
					ways+=dp[times-1][j-i];
				}
			}
			dp[times][j]=ways;
		}
	}
	//int sum=dp[K][N];
	int sum=f(N,M,K);
	double result=(double)sum/(double)pow((M+1),K);
	cout<<result;
	return 0;
}

可以看到在填表时有枚举行为,所以还可以继续优化

举个例子:M等于7,如果times等于5,j等于7,那么该位置依赖:dp[4][7,6 5 4 ……0];
如果times等于5,j等于6,那么该位置依赖:dp[4][6 5 4 3 ……0 -1]。
所以dp[i][j]=dp[i][j-1]+dp[i-1][j]-dp[i-1][j-M-1],注意最后的j-M-1如果小于0了依然要算进去,只不过要用公式算

#include<iostream>
#include<cmath>
#include<vector>
using namespace std;

//当前怪兽还有N滴血,还剩K刀
//在0-M范围上砍血
//返回当前被该死的情况数
int f(int N,int M,int K) {
	if(K==0) { //如果血量小于等于0,说明已经砍死。(注意如果当前怪兽已经是小于等于0了,但是K大于0,依然要继续砍,因为被砍到0了,之后的情况依然要算上
		return N<=0?1:0;
	}
	if(N<=0) {
		return pow(M+1,K);//死亡的情况数
	}
	int ways=0;
	for(int i=0; i<=M; i++) {
		ways+=f(N-i,M,K-1);
	}
	return ways;
}
int main() {
	int N;
	int M;
	int K;
	cin>>N>>M>>K;
	vector<vector<int>>dp(K+1,vector<int>(N+1));
	dp[0][0]=1;
	for(int times=1; times<=K; times++) {
		dp[times][0]=pow(M+1,times);//第一列符合递归的终止条件,可以直接计算出来
		for(int j=1; j<=N; j++) {
			dp[times][j]=dp[times][j-1]+dp[times-1][j];
			if(j-M-1>=0) {
				dp[times][j]-=dp[times][j-M-1];
			} else {
				dp[times][j]-=pow(M+1,times-1);//注意越界了的情况
			}
		}
	}
	int sum=dp[K][N];
	//int sum=f(N,M,K);
	double result=(double)sum/(double)pow((M+1),K);
	cout<<result;
	return 0;
}

  1. 剑指 Offer II 103. 最少的硬币数目

分析:从左往右的尝试模型
注意在更新最小个数时的操作

class Solution {
public:
//返回从index开始选择硬币来构成rest大小最小需要多少张
    int f(vector<int>&coins,int index,int rest){
        if(index==coins.size()){
            return rest==0?0:INT_MAX;//当前没有硬币可以选了且rest为0,返回0
        }
        int minAmount=INT_MAX;
        for(int t=0;t*coins[index]<=rest;t++){//从0张开始枚举
            int next=f(coins,index+1,rest-t*coins[index]);//从index开始选择硬币来构成rest大小最小需要多少张
            if(next!=INT_MAX){
                minAmount=min(minAmount,t+next);
            }
        }
        return minAmount;
    }
    int coinChange(vector<int>& coins, int amount) {
        return f(coins,0,amount)==INT_MAX?-1:f(coins,0,amount);
    }
};

严格表结构的动态规划 (版本一 含枚举行为)

class Solution {
	public:
		int coinChange(vector<int>& coins, int amount) {
			int n=coins.size();
			vector<vector<int>>dp(n+1,vector<int>(amount+1));
			for(int rest=1; rest<=amount; rest++) {
				dp[n][rest]=INT_MAX;
			}
			for(int index=n-1; index>=0; index--) {
				for(int rest=0; rest<=amount; rest++) {
					int minAmount=INT_MAX;
					for(int t=0; t*coins[index]<=rest; t++) {				            int next=dp[index+1][rest-t*coins[index]];
						if(next!=INT_MAX) {
							minAmount=min(minAmount,t+next);
						}
					}
                    dp[index][rest]=minAmount;
				}

			}
            return dp[0][amount]==INT_MAX?-1:dp[0][amount];
		}
};

改进版的动态规划
其实这个枚举行为也是可以省去的
假设index=3,coins[3]=2,rest=10
所以要填3,10位置的值依赖:4,10;4,8;4,6;4,4;4,2;4,0这几个的最小值加个数
而3,8位置的值依赖:,8;4,6;4,4;4,2;4,0这几个的最小值加个数。注意这个和上一个个数是差1的,最后不要忘了加1

class Solution {
public:
    int f(vector<int>coins,int index,int rest){
        if(index==coins.size()){
            return rest==0?0:INT_MAX;
        }
        int minAmount=INT_MAX;
        for(int t=0;t*coins[index]<=rest;t++){
            int next=f(coins,index+1,rest-t*coins[index]);
            if(next!=INT_MAX){
                minAmount=min(minAmount,t+next);
            }
        }
        return minAmount;
    }
    int coinChange(vector<int>& coins, int amount) {
        int n=coins.size();
			vector<vector<int>>dp(n+1,vector<int>(amount+1));
			for(int rest=1; rest<=amount; rest++) {
				dp[n][rest]=INT_MAX;
			}
			for(int index=n-1; index>=0; index--) {
				for(int rest=0; rest<=amount; rest++) {
                    int minAmount=INT_MAX;
                    if(rest-coins[index]>=0){
                        minAmount=dp[index][rest-coins[index]];
                        if(minAmount!=INT_MAX){//个数加1
                            minAmount++;
                        }
                    }      
                    minAmount=min(minAmount,dp[index+1][rest]);
                    dp[index][rest]=minAmount;
				}

			}
            return dp[0][amount]==INT_MAX?-1:dp[0][amount];

       
    }
};

  1. 给定一个正数n,求n的裂开方法数。规定:后面的数不能比前面的数小 。比如4的裂开方法有: 1+1+1+1、1+1+2、1+3、2+2、4,5种,所以返回5

分析:从左往右尝试,假设当前选择了index,那么后续都要从index开始选择,新的目标值就是aim-index

int f(int index,int aim) {

	if(aim==0) { //找到了一个方法
		return 1;
	}
	if(index>aim){
		return 0;
	}
	
	int ways=0;
	for(int i=index; i<=aim; i++) {
		ways+=f(i,aim-i);//选择了i,后续从i到aim-i选择,每次选择都是非递减的,所以不会出现重复
	}
	return ways;
}

含枚举行为的动态规划

int splitNum(int num) {
	if(num<=0) {
		return 0;
	}
	vector<vector<int>>dp(num+1,vector<int>(num+1));
	for(int index=1; index<=num; index++) {
		dp[index][0]=1;
	}
	for(int index=num; index>=1; index--) {
		for(int aim=index; aim<=num; aim++) {//左下角的元素都是0
			int ways=0;
			for(int i=index; i<=aim; i++) {
				ways+= dp[i][aim-i];//选择了i,后续从i到aim-i选择,每次选择都是非递减的,所以不会出现重复
			}
			dp[index][aim]= ways;
		}
	}

	return dp[1][num];

}

最终版的动态规划
第一处优化:对角线都是1
第二处:省去枚举行为,p[index][aim]和dp[index+1][aim]其实就差了一个dp[index][aim-index]

#include<iostream>
#include<vector>
using namespace std;

int splitNum(int num) {
	if(num<=0) {
		return 0;
	}
	vector<vector<int>>dp(num+1,vector<int>(num+1));
	for(int index=1; index<=num; index++) {
		dp[index][0]=1;
		dp[index][index]=1;//对角线都是1
	}
	for(int index=num-1; index>=1; index--) {//从倒数第二行开始填
		for(int aim=index+1; aim<=num; aim++) {
			dp[index][aim]=dp[index+1][aim]+dp[index][aim-index];//不难发现dp[index][aim]和dp[index+1][aim]其实就差了一个dp[index][aim-index]		
		}
	}

	return dp[1][num];

}


int main() {

	cout<<splitNum(13);
	return 0;
}

总结:当填表有枚举行为时,就分析它旁边的位置,通常都可以替代枚举行为。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值