左程云算法基础班整理(十二)(c++版)

十二.左程云算法基础班整理(十二)(c++)

几个模型反复练
3条主线,暴力递归一旦改出来,怎么往下优化。
设计暴力递归怎么知道靠谱不靠谱
给4个模型,往上面编。

按照设计原则来往尝试模型上套

常见的四种尝试模型
1.从左往右的尝试模型(背包问题,从左往右考虑每个位置要还是每个位置不要,数字转换成字母的问题,考虑多大一串数字转换成字母)
2.范围上的尝试模型(纸牌博弈问题)
3.多样本位置全对应的尝试模型
4.寻找业务限制的尝试模型

如何分析有没有重复解?

列出调用过程,可以只列出前几层
有没有重复解,一看便知

如何找打某个问题的动态规划方式?
1.设计暴力递归:重要原则+4种常见尝试模型!重点!!!
2.分析有没有重复解:套路解决
3.用记忆化搜索->用严格表结构实现动态规划:套路解决
4.看看能否继续优化:套路解决

怎么猜才能猜得对?

面试中暴力递归的设计原则
1.每一个可变参数的类型,一定不要比int更加复杂
2.原则1可以违反,让类型突破到一维线性结构,那必须是唯一可变参数(比如线性字符串)
3.如果发现原则1被违反,但不违反原则2,只需要做到记忆化搜索即可
4.可变参数的个数,能少则少

如果可变参数是int a,则是一张一维表。如果可变参数是int a,int b,则dp是一张二维表。如果可变参数一个是int a,一个是bool b,则dp表有两张,为true时一张一维表,为false时一张一维表。

不可能让设计数组的参数。

在这里插入图片描述

暴力递归改动态规划如下所示。
如果递归过程中无枚举行为,意思是依赖的是优先的状态,记忆化搜索傻缓存,cache就可以了,不用再改动态规划,两者时间复杂度是一样的。但是表中有枚举行为的话,要转成经典动态规划,因为后续可能会有新的优化,枚举是指for或者while循环枚举。

在这里插入图片描述

12.1斐波那契数列

什么暴力递归可以继续进行优化?
有重复调用同一个子问题的解,这种递归可以进行优化。
如果每一个子问题都是不同的解,无法优化也不用优化。

斐波那契数列,可以进行优化。
暴力递归为什么暴力?
因为有大量的重复计算过程。比如计算斐波那契的第七项F(7);在这里插入图片描述
再次计算F(5),就会浪费大量时间。因此可以用一个缓存结构将下面的结果存下来,可以带来时间上的大量优化。

#include<iostream>
using namespace std;

int f(int n)
{
	if (n == 1)
	{
		return 1;
	}
	if (n == 2)
	{
		return 1;
	}
	return f(n - 1) + f(n - 2);
}
int main()
{
	cout << f(10) << endl;

	system("pause");
	return 0;
}

斐波那契数列可以用一种方法把O(N)的时间复杂度优化成log(N)的。

12.2机器人行走(阿里巴巴原题)

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

N是几个位置,M是开始点,P是结束点,K是经过了几步。
例如,F(6,3,3,4)的构成
在这里插入图片描述
其中有很多过程可以进行重复使用,因此可以用一个缓存将其记录下来,加快速度。
在这里插入图片描述
如上图所示,其中f(3,2)重复了很多次,完全可以用一个缓存将其记录下来,之后不用再次计算,直接在里面取值就行了。

int waysCache(int N, int M, int K, int P)
{
	//参数无效直接返回0
	if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) 
	{
		return 0;
	}
	//可以根据实际题解进行初始化
	vector<vector<int>> dp(N + 1, vector<int>(K + 1));
	for (int row = 0; row <= N; row++)
	{
		for (int col = 0; col <= K; col++)
		{
			dp[row][col] = -1;//先初始化为-1,表示当前的组合还没有算过。
		}
	}
	return walkCache(N, M, K, P, dp);
}
int walkCache(int N, int cur, int rest, int P, vector<vector<int>>& dp)//要指出后面的来
{
	if (dp[cur][rest] != -1)
	{
		return dp[cur][rest];
	}
	if (rest == 0)
	{
		dp[cur][rest] = cur == P ? 1 : 0;
		return dp[cur][rest];
	}
	if (cur == 1)
	{
		dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp);
		return dp[cur][rest];
	}
	if (cur == N)
	{
		dp[cur][rest] = walkCache(N, N - 1, rest - 1, P, dp);
		return dp[cur][rest];
	}
	dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P, dp)
		+ walkCache(N, cur - 1, rest - 1, P, dp);
	return dp[cur][rest];
}

这就是动态规划,什么是动态规划,递归过程中有重复过程,加缓存记录下来,下次直接用,这就是动态规划。
遇到重复的解,就去拿缓存里的东西,如果没算过,那就去算,这就是动态规划。
这道题的动态规划是一张二维表,因为两个可变参数,所有组合都要去求。
找依赖关系,箭头表示了依赖关系。

看着暴力递归过程,根据表改动态规划。
在这里插入图片描述在这里插入图片描述
把暴力递归的分析过程抽象出来,都会得到动态规划。
有限参数是N个,则dp就是一个N维表。
只要能试出由可变参数改成的暴力递归,就能改成动态规划。
不是所有暴力递归都能改成动态规划,而是所有动态规划都来自某些暴力递归。
有一些暴力递归无法改成动态规划是因为没有足够的重复的过程(因为没有大量的使用重复解。)
其实就是把参数组合完成了结构化的。

//根据递归过程改动态规划
int ways2(int N,int M,int K,int P)
{
	// 参数无效直接返回0
	if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
		return 0;
	}

	vector<vector<int>> dp(N + 1, vector<int>(K + 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];
}
12.3背包问题改动态规划

背包问题改动态规划,因为会有重复利用的问题,所以说会有动态规划的意义的存在。
下图就说明了重复在哪。
在这里插入图片描述

一旦写出暴力递归,那么递归就能改动态规划,不用再看原始题意就可以,解耦了。
背包问题,最后要的是dp表(0,bag)位置的返回值。

所谓动态规划转移方程,其实就是暴力递归的点。
把两个可变参数变化范围整理清楚后,生成一张二维表。
在这里插入图片描述
有了递归后,改动态规划和原题意没有任何关系,看着递归过程就能改出来。

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

// 只剩下rest的空间了,
// index...货物自由选择,但是剩余空间不要小于0
// 返回 index...货物能够获得的最大价值
int process(vector<int>& w, vector<int>& v, int index, int rest)
{
	if (rest < 0)
	{
		return -1;
	}
	if (index == w.size())
	{
		return 0;
	}
	//背包既有空地方又没选完呢
	int p1 = process(w, v, index + 1, rest);//没选
	int p2 = -1;//先假设p2方案无效
	int p2next = process(w, v, index + 1, rest - w[index]);//index位置上的物品要了,因此剩余的物品重量要减去,但是之后要确认负重是否合理
	if (p2next != -1)
	{
		p2 = v[index] + p2next;//当p2Next是有效的时候,p2修改为有效
	}
	return max(p1, p2);
}
int dpWay(vector<int>& w, vector<int>& v, int bag)
{
	int N = w.size();
	//里面必须要填上常量
	vector<vector<int>> dp(N + 1, vector<int>(bag + 1));
	for (int i = 0; i < 12; i++)
	{
		dp[N][i] = 0;
	}
	for (int index = N - 1; index >= 0; index--)//第一个for循环规定了从下面行往上面行开始填
	{
		for (int rest = 0; rest <= bag; rest++)// rest < 0  从左往右填好每一行
		{
			//在每一个循环体内,都把index和rest定了下来,其实就是求dp[index][rest]?
			
			int p1 = dp[index + 1][rest];
			int p2 = -1;
			if (rest - w[index] > 0)//保证数组内的不越界
			{
				p2 = v[index] + dp[index + 1][rest - w[index]];//后面那个要保证不越界才行
			}
			dp[index][rest] = max(p1, p2);
	
			/*
			//背包既有空地方又没选完呢
			int p1 = process(w, v, index + 1, rest);//没选
			int p2 = -1;//先假设p2方案无效
			int p2next = process(w, v, index + 1, rest - w[index]);//index位置上的物品要了,因此剩余的物品重量要减去,但是之后要确认负重是否合理
			if (p2next != -1)
			{
				p2 = v[index] + p2next;//当p2Next是有效的时候,p2修改为有效
			}
			return max(p1, p2);
			*/
		}
	}
	return dp[0][bag];
}
int maxValue(vector<int>& w, vector<int>& v, int bag)
{
	return process(w, v, 0, bag);
}
int main()
{
	vector<int> weights = { 3,2,4,7 };
	vector<int> values = { 5,6,3,19 };

	int bag = 11;
	cout << dpWay(weights, values, bag) << endl;
	cout << maxValue(weights, values, bag) << endl;
	system("pause");
	return 0;
}
12.4字符串转数字动态规划

只有一个可变参数,所以是一维表,根据初始信息,可以后面的信息。
在这里插入图片描述
毫无疑问,这张表要从右往左填。

这里有个小疑问,一维表比大小多了1个?这是为啥

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


// str[0...i-1]已经转化完了,固定了
// i之前的位置,如何转化已经做过决定了, 不用再关心
// i... 有多少种转化的结果
int process(string str, int index)
{
	if (index == str.size())// base case
	{
		return 1;//终止条件,返回1,,可以进行相加
	}
	//i没有到终止位置
	if (str[index] == '0')
	{
		return 0;
	}	//0没有办法转化,1对应a,26对应z,0是没法转换的,所以只要i位置出现0,就不用管后面的情况,直接return 0退出就行。
	//i没有到终止位置
	//str[i]字符不是‘0’
	if (str[index] == '1')
	{
		int res = process(str, index + 1);//i自己作为单独的部分,后续有多少种方法
		if (index + 1 < str.size())
		{
			res += process(str, index + 2);//i和i+1作为单独的部分,后续有多少种方法
		}
		return res;
	}
	if (str[index] == '2')
	{
		int res = process(str, index + 1); //i自己作为单独的部分,后续有多少种方法
		//i和i+1作为单独的部分并且没有超过26.后续有多少种方法。
		if (index + 1 < str.size() && str[index + 1] >= '0' && str[index + 1] <= '6')
		{
			res += process(str, index + 2);// (i和i+1)作为单独的部分,后续有多少种方法
		}
		return res;
	}
	//str[i] == ‘3’~‘9’,不可能和i+1位置共同进行转换了,因为肯定会超过26.
	return process(str, index + 1);
}
int dpWay(string str)
{
	int n = str.size();
	if (n == 0)return 0;
	//只有index是一个可变参数,从0到n-1
	vector<int> dp(n+1,0);
	dp[n] = 1;
	for (int i = n - 1; i >= 0; i--)
	{
		//dp[i] = ?
		if (str[i] == '0')
		{
			dp[i] = 0;
		}
		if (str[i] == '1')
		{
			dp[i] = dp[i + 1];
			if (i + 1 < n)
			{
				dp[i] += dp[i + 2];
			}
		}
		if (str[i] == '2')
		{
			dp[i] = dp[i + 1];
			if ((i + 1) <n && str[i + 1] >= '0' && str[i + 1] <= '6')
			{
				dp[i] += dp[i + 2];
			}
		}
	}
	return dp[0];
}
int number(string str)
{
	if (str.size() == 0)
	{
		return 0;
	}
	return process(str, 0);
}
int main()
{
	cout << number("111") << endl;
	cout << dpWay("111") << endl;
	system("pause");
	return 0;
}
12.5卡牌问题转动态规划

两个参数L和R代表卡牌的范围。
所以如果是正方形的表,如果F和S是两个过程的话,则对应两张正方形的表。
并且因为L>R 时无效,所以半张表是无效的。

在这里插入图片描述
用f的对象线来推s对角线上一条,用s对角线来推f对角线上一条,以此类推。
两个表交替往上推,推到星号就可以结束了。
只要从已知推得星号位置的信息,就可以了。
在这里插入图片描述

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


int f(vector<int>& card, int L, int R);
int s(vector<int>& card, int L, int R);
int win(vector<int>& card)
{
	if (card.size() == 0)return 0;
	return max(f(card, 0, card.size() - 1), s(card, 0, card.size() - 1));
}
// L....R
// F  S  L+1..R
// L..R-1
//L和R是用来在范围中拿值的
int f(vector<int>& card, int L, int R)
{
	if (L == R)return card[L];
	return max(card[L] + s(card, L + 1, R), card[R] + s(card, L, R - 1));
}
//后手的选择权并不在自己,因此对方一定是按后手的效益最低化来选取的,所以说是min,你没得选,对手丢给你两者中最小的
int s(vector<int>& card, int L, int R)
{
	if (L == R)return 0;
	return min(f(card, L, R - 1), f(card,L + 1, R));
}
//这里又是n的大小了。
int win2(vector<int>& card)
{
	int n = card.size();
	vector<vector<int>> f(n, vector<int>(n));
	vector<vector<int>> s(n, vector<int>(n));

	for (int i = 0; i < n; i++)
	{
		f[i][i] = card[i];
	}
	//s[i][i]=0;
	for (int i = 1; i < n; i++)
	{
		int L = 0;
		int R = i;
		while (L < n && R < n)
		{
			//dp[L][R] = ?

			f[L][R] = max(card[L] + s[L + 1][R], card[R] + s[L][R - 1]);
			s[L][R] = min(f[L + 1][R], f[L][R - 1]);
			L++;
			R++;
		}
	}
	return max(f[0][n - 1], s[0][n - 1]);
}
int main()
{

	vector<int> card = { 4,7,9,5,19,29,80,4 };
	cout << win(card) << endl;
	cout << win2(card) << endl;

	system("pause");
	return 0;
}
12.6钱的方法数

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

后效性:前面的选择影响后面,比如一个1*2的网格往正方形的方格里面摆。多少种摆法。如下的摆法,不管怎么摆都是一样的。
在这里插入图片描述
在这里插入图片描述
可变参数选对了,前面的选择不会影响后面的状态,只要看状态就行了,就是无后效性的。类似于状态同步。

整体顺序从小往上,具体每一行从左往右。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

12.6.1暴力递归方法实现
#include<iostream>
#include<vector>
using namespace std;

int process(vector<int>& money, int index, int rest);
int process1(vector<int>& money, int index, int rest, vector<vector<int>>& dp);

//money中都是正数且无重复值,返回组成aim的方法数
int ways1(vector<int>& money, int aim)
{
	if (money.size() == 0 || aim < 0)
	{
		return 0;
	}
	return process(money, 0, aim);//直接返回,当1张还没选时,目标是aim的方法数
}

//暴力递归过程
//可以自由使用money[index...]所有的面值,每一种面值都可以使用任意张,
//组成rest,有多少种方法
int process(vector<int>& money, int index, int rest)
{
	if (rest < 0)
	{
		return 0;//其实由于下面,rest -  (zhang * money[index])肯定>0,所以说这里可以不用写。
	}
	//rest>=0;
	if (index == money.size())//没有货币可以进行选择了
	{
		return rest == 0 ? 1 : 0;
	}
	//当前有货币,money[index]
	int ways = 0;
	for (int zhang = 0; zhang * money[index] <= rest; zhang++)
	{
		ways += process(money, index + 1, rest - (zhang * money[index]));
	}
	return ways;
}
12.6.2记忆化搜索方式(自顶向下的的动态规划)

傻缓存也是动态规划的一种,只不过不够精细化,有点傻= 。=
傻缓存思路很简单,算过的直接去缓存里拿,没算过的就算了之后存进去。

//记忆化搜索过程
int ways2(vector<int>& money, int aim)
{
	int n = money.size();
	if (money.size() == 0||aim<0)
	{
		return 0;
	}

	vector<vector<int>> dp(n + 1, vector<int>(aim + 1,-1));
	//一开始所有的过程,都没有计算呢
	//dp[..][..]
	return process1(money, 0, aim, dp);
}

// 如果index和rest的参数组合,是没算过的,dp[index][rest] == -1
// 如果index和rest的参数组合,是算过的,dp[index][rest]  > - 1
// 这叫自顶向下的动态规划,没算过的我就给他算,算过的就给他值。
int process1(vector<int>& money, int index, int rest, vector<vector<int>>& dp)
{
	if (dp[index][rest] != -1)
	{
		return dp[index][rest];
	}
	if (index == money.size())
	{
		dp[index][rest] = rest == 0 ? 1 : 0;//在return之前要把dp表进行更新
		return dp[index][rest];
	}
	int ways = 0;
	for (int zhang = 0; zhang * money[index] <= rest; zhang++)
	{
		ways += process1(money, index + 1, rest - (zhang * money[index]), dp);
	}
	dp[index][rest] = ways;
	return ways;
}
12.6.3正版动态规划
//第一版动态规划,但是每一行有枚举行为,和记忆化搜索等效,因为有枚举行为,所以还是可以进行优化的。
int ways3(vector<int>& money, int aim)
{
	int n = money.size();
	if (n == 0||aim<0) return 0;

	vector<vector<int>> dp(n + 1, vector<int>(aim + 1, 0));
	dp[n][0] = 1;// dp[N][1...aim] = 0;dp[n][0]是指已经选了n种钱了,此时剩余0元要选,符合题意。
	
	//怎么得到dp[index][rest]?
	for (int index = n - 1; index >= 0; index--)
	{
		for (int rest = 0; rest <= aim; rest++)
		{
			int ways = 0;
			for (int zhang = 0; zhang * money[index] <= rest; zhang++)
			{
				ways += dp[index + 1][rest - (zhang * money[index])];
			}//预处理数组的某些题,斜率优化的某些题,四边形不等式的某些题,都是可以优化的。
			dp[index][rest] = ways;
		}
	}
	return dp[0][aim];
}
12.6.4优化后的动态规划
int ways4(vector<int>& money, int aim)
{
	int n = money.size();
	if (n == 0||aim < 0) return 0;

	vector<vector<int>> dp(n + 1, vector<int>(aim + 1, 0));
	dp[n][0] = 1;

	for (int index = n - 1; index >= 0; index--)
	{
		for (int rest = 0; rest <= aim; rest++)
		{
			dp[index][rest] = dp[index + 1][rest];
			if (rest - money[index] >= 0)
			{
				dp[index][rest] += dp[index][rest - money[index]];
			}
		}
	}
	return dp[0][aim];
}

12.7贴纸问题

问题:给定一个字符串str,给定一个字符串类型的数组arr。
arr里的每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。
返回需要至少多少张贴纸可以完成这个任务。
例子:str= “babac”,arr={“ba”,“c”,“abcd”}
至少需要两张贴纸‘ba’和“abcd”,因为使用这两张贴纸,把每一个字符单独剪开,含有2个a,2个b和1个c,是可以拼出str的。所以返回2

最简单的方法,先排个序。选择第一张贴纸,进行枚举。搞定rest的第一张贴纸是什么。
一开始先判断数组中包含的字符能否覆盖该问题,没有直接返回假。
不要跳过暴力递归过程直接来动态转移。
在这里插入图片描述
没必要留贴纸的字符串,我们只需要留下词频就可以。完全可以转化成数组。数组长度26,对应26个字母。
在这里插入图片描述
等价生成一个代表词频的数组。
这里的傻缓存叫rest。
傻缓存的思路就是,算过的去用,没算过的去算。

在这里插入代码片

这道题,第一轮选aa,第二轮bb,和第一轮bb,第二轮aa,对第三轮的选择是没有影响的,如果能记录下来,就可以加速,因此可以改为动态规划。
但是rest的可能性有无数个,因此精细化的动态规划没必要,所以傻缓存就可以了。
细粒度的种类太多了,不好枚举。

continue那个地方,起到了贪心加速的过程,但是不能去掉。比如消不掉xyz的话,不加速的话,会无限循环。
在这里插入图片描述
设计递归的原则,可变参数越少越好,越少dp表的维数越低,缓存命中率越高,越好写。

12.8两个字符串的最长公共子序列(多样本位置全对应问题)

题目类似下图所示,两个字符串,都包含1,2,3,4,5,且是其最大的公共子序列(序列和是否连续是无关的)
在这里插入图片描述
两个样本的对应模型,就是一个字符串做行,一个字符串做列,两个对应成一个二维表,全对应。

可以设计一个f,f(str1,i1,str2,i2)。表示到了str1的i1位置,str2的i2位置
但是这里左神为了节省时间,直接就对表下手了,dp[i][j]的含义是,str1[0…i],str2[0…j]的公共子序列有多长,那个int值放到dp表内。
在这里插入图片描述
对于这道题,我们要找的是dp[5][6];
先解决边界问题,也就是第一行和第一列。
之后在想办法去找dpij;
在这里插入图片描述
有四种可能的情况
1.最长公共子序列不以i和j结尾。在都不结尾的情况下,与i,j位置没关系。也就是说依赖于i-1,j-1的位置的情况。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.最长公共子序列以i结尾,不以j结尾。
第二种依赖左侧格子的解。
在这里插入图片描述
3.最长公共子序列以j结尾,不以i结尾。与2同理,依赖的是上面格子。
在这里插入图片描述

4.最长公共子序列,以i,j位置结尾。
在这里插入图片描述
按照最长公共子序列结尾在哪进行的组织的可能性。
在这里插入图片描述

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

int lcse(string str1,string str2)
{
	int m = str1.size();
	int n = str2.size();
	vector<vector<int>> dp(m, vector<int>(n));
	dp[0][0] = str1[0] == str2[0] ? 1 : 0;

	for (int i = 1;i< str1.size(); i++)
	{
		//下边那个表示,一旦出现了相同字符,则后面的全是1
		dp[i][0] = max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
	}
	for (int j = 1; j < str2.size(); j++)
	{
		//下边那个表示,一旦出现了相同字符,则下面的全是1
		dp[0][j] = max(dp[0][j-1], str1[0] == str2[j] ? 1 : 0);
	}
	for (int i = 1; i < m; i++)
	{
		for (int j = 1; j < n; j++)
		{
			dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
			if (str1[i] == str2[j])
			{
				dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
			}
		}
	}
	return dp[m-1][n-1];
}
int main()
{

	system("pause");
	return 0;
}
12.9寻找业务限制的尝试模型

题目:给定一个数组,代表每个人喝完咖啡准备刷杯子的时间
只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯。
每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发
返回让所有咖啡杯变干净的最早完成时间
三个参数,vector
coffee ,int a,int b;
只要决定要洗,就不能再挥发了。

挥发可以是并行的,但是洗咖啡杯是串行的。
这是一个从左往右的尝试模型,也是一个业务限制的模型。

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

//process(drinks,3,10,0,0)
//a 洗一杯的时间
//b 自己挥发干净的时间,固定变量
//drinks 每一个员工喝完的时间,固定变量
//drinks[0..index-1]都已经干净了,不用你操心了
//drinks[index...]都想变干净,这是我操心的,washLine表示洗的机器何时可用
//drinks[index...]变干净,最少的时间点返回
//从左往右的尝试模型加业务限制模型


//只有index和washine是可变参数。
int process(vector<int>& drinks,int a,int b,int index,int washLine)
{//关心咖啡机来到什么时候就行了,washline最夸张的情况是全洗

	//n是员工个数
	int n = drinks.size();
	if (index == n - 1)
	{//min比较的是洗一杯和挥发的时间最小值。其中洗又要选喝完和机器的最大值。
		return min(max(washLine, drinks[index]) + a, drinks[index] + b);
	}


	// 剩不止一杯咖啡
	// wash是我当前的咖啡杯,
	int wash = max(washLine, drinks[index]) + a;// 洗,index一杯,结束的时间点
	// index+1...变干净的最早时间
	int next1 = process(drinks, a, b, index + 1, wash);//如果index选择洗的话,下一杯也就是index+1及其之后的杯子变干净的话最早时间点。
	

	//所以index往后的最短时间,即要index洗完,也要求index+1即往后洗完.
	int p1 = max(wash, next1);

	
	int dry = drinks[index] + b;//当前杯index挥发的时间
	int next2 = process(drinks, a, b, index + 1, washLine);//在前一杯index选择挥发的前提下,index+1及其后面变干净的最早时间点
	
	//所以index往后的最短时间,即要index挥发完,也要求index+1及往后都干净.
	int p2 = max(dry, next2);


	return min(p1, p2);
}
int dp(vector<int>& drinks,int a,int b)
{
	int n = drinks.size();
	//洗一杯的时间比挥发的时间长,那直接都挥发就行了。。。。
	if (a >= b)
	{
		return drinks[n - 1] + b;//挥发的时间比洗的时间短,还洗个球球
	}
	//a<b
	int limit = 0;// 咖啡机什么时候可用
	for (int i = 0; i < n; i++)
	{
		limit = max(limit, drinks[i]) + a;washline的极限,是不可能超过limit的
	}
	vector<vector<int>> dp(n, vector<int>(limit + 1));//0-limit都可能,所以是limit+1
	//n-1行,所有的值
	for (int washLine = 0; washLine <= limit; washLine++)
	{
		dp[n - 1][washLine] = min(max(washLine, drinks[n - 1]) + a, drinks[n - 1] + b);
	}
	for (int index = n - 2; index >= 0; index--)
	{
		for (int washLine = 0; washLine <= limit; washLine++)
		{
			int p1 = INT16_MAX;
			int wash = max(washLine, drinks[index]) + a;
			if (wash <= limit)//怎么知道p1存在不存在,只有左边的不越界才存在。
			{
				p1 = max(wash, dp[index + 1][wash]);
			}
			//p2不会越界,因为washLine一直没有变化。
			int p2 = max(drinks[index] + b, dp[index + 1][washLine]);
			dp[index][washLine] = min(p1, p2);
		}
	}
	return dp[0][0];
}

int main()
{
	vector<int> test = { 1,1,5,5,7,10,12,12,12,12,12,12,15 };
	int a1 = 3;
	int b1 = 10;
	cout << process(test, a1, b1, 0, 0) << endl;
	cout << dp(test, a1, b1) << endl;

	system("pause");
	return 0;
}
12.9跳马棋的题目

在象棋中,有一个竖着10条线,横着9条线的棋盘。
假设马在最左下的位置,马走日。
马从开始跳K步,跳到(x,y)点,问总共有多少种跳法。
在这里插入图片描述
递归函数这样就可以了。走k步,来到x,y的位置,问多少种方法。
可变参数有三个,当前位置的横纵坐标,和步数。
在这里插入图片描述
哪些位置能来到X,Y位置呢?
邻近的8个位置。
只要在k-1步的时候来到了邻近的8个位置之一,那下一步一定可以到达该地方。
在这里插入图片描述

int ways(int a, int b, int step)
{
	return f(0,0,step,a,b);
}
//马初始是在(0,0),要走step步,走到(a,b)的位置,最终要来到x,y的位置上的方法数是多少种,i,j是当前的位置
int f(int i, int j, int step,int a,int b)
{
	if (step == 0)
	{
		return (i == a && j == b) ? 1 : 0;
	}
	if (i < 0 || i>9 || j < 0 || j>8)
	{
		return 0;
	}
	//有步数要走,X,y也是棋盘上的位置
	return f(i - 2, j + 1, step - 1, a, b)
		+ f(i - 1, j + 2, step - 1, a, b)
		+ f(i + 1, j + 2, step - 1, a, b)
		+ f(i + 2, j + 1, step - 1, a, b)
		+ f(i + 2, j - 1, step - 1, a, b)
		+ f(i + 1, j - 2, step - 1, a, b)
		+ f(i - 1, j - 2, step - 1, a, b)
		+ f(i - 2, j - 1, step - 1, a, b);
}

改成动态规划,这里要注意,8个位置,每个位置的判定时,都有两种终止可能,一是返回1,二是直接越界返回0;所以不如直接写成个函数,好判别。

int getValue(vector<vector<vector<int>>>& dp, int i, int j, int step)
{
	if (i < 0 || i>9 || j < 0 || j>8)
	{
		return 0;
	}
	return dp[i][j][step];
}
int dpWay(int a, int b, int s)
{
	//i,j,0-step
	vector<vector<vector<int>>> dp(10, vector<vector<int>>(9, vector<int>(s + 1)));

	dp[a][b][0] = 1;

	for (int step = 1; step <= s; step++)
	{
		for (int i = 0; i < 10; i++)
		{
			for (int j = 0; j < 9; j++)
			{
				dp[i][j][step] = getValue(dp, i - 2, j + 1, step - 1)
					+ getValue(dp, i - 1, j + 2, step - 1)
					+ getValue(dp, i + 1, j + 2, step - 1)
					+ getValue(dp, i + 2, j + 1, step - 1)
					+ getValue(dp, i + 2, j - 1, step - 1)
					+ getValue(dp, i + 1, j - 2, step - 1)
					+ getValue(dp, i - 1, j - 2, step - 1)
					+ getValue(dp, i - 2, j - 1, step - 1);
			}
		}
	}
	return dp[0][0][s];
}

int main()
{
	int x = 7;
	int y = 7;
	int step = 10;
	cout << ways(x, y, step) << endl;
	cout << dpWay(x, y, step) << endl;


	system("pause");
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值