当填表时有枚举行为,就可能涉及到优化,通常就是斜率优化。
- 问题描述:给出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;
}
- 问题描述:给出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;
}
分析:从左往右的尝试模型
注意在更新最小个数时的操作
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];
}
};
- 给定一个正数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;
}
总结:当填表有枚举行为时,就分析它旁边的位置,通常都可以替代枚举行为。