【算法系列】动态规划详解(附LeetCode例题)

一、动态规划原理

1、基本思想

问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,在构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上从最终子问题向原问题逐步求解。

2、使用条件:

可分为多个相关子问题,子问题的解被重复使用

Optimal substructure(最优子结构):
一个问题的优化解包含了子问题的优化解,缩小子问题集合,只需那些优化问题中包含的子问题,降低实现复杂性 我们可以自下而上的
Subteties(重叠子问题):在问题的求解过程中,很多子问题的解将被多次使用。

3、动态规划算法的设计步骤

  • 分析优化解的结构
  • 递归地定义最优解的代价
  • 自底向上地计算优化解的代价保存之,并获取构造最优解的信息
  • 根据构造最优解的信息构造优化解

4、动态规划特点

  • 把原始问题划分成一系列子问题;
  • 求解每个子问题仅一次,并将其结果保存在一个表中,以后用到时直接存取,不重复计算,节省计算时间
  • 自底向上地计算。
  • 整体问题最优解取决于子问题的最优解(状态转移方程)(将子问题称为状态,最终状态的求解归结为其他状态的求解)

二、LeetCode例题

1、word-break-ii

(1)题目描述:
给定一个字符串s和一组单词dict,在s中添加空格将s变成一个句子,使得句子中的每一个单词都是dict中的单词
返回所有可能的结果
例如:给定的字符串

s =“catsanddog”, dict =[“cat”, “cats”, “and”, “sand”, “dog”].

返回的结果为

[“cats and dog”, “cat sand dog”].

Question:

Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.
Return all such possible sentences.
For example, given s =“catsanddog”, dict =[“cat”, “cats”, “and”,
“sand”, “dog”].
A solution is[“cats and dog”, “cat sand dog”].

(2)思路分析:
利用一维容器数组存储字典中出现过的word,并且记为True,未出现记为False

vector<type_of_bool> *cstr = new vector<type_of_bool>[s.length()];

*cstr0123456789
0ccacatcatscatsacatsancatsandcatsanddcatsanddocatsanddog
1aatatsatsaatsanatsandatsanddatsanddoatsanddog
2ttstsatsantsandtsanddtsanddotsanddog
3ssasansandsanddsanddosanddog
4aanandanddanddoanddog
5nndnddnddonddog
6dddddoddog
7ddodog
8oog
9g

递归遍历顺序是先搜索i到串尾的子串,若子串在dict里,再搜索串头到i的子串。
利用mystring临时存储每次遍历得到的正确分割,result存储所有可能的结果构成的字符串。

(3)牛客网线上测试代码以及注释:

class Solution {
private:
	vector<string> mystring;//临时存储每次遍历得到的正确分割
	vector<string> result;//存储所有可能的结果构成的字符串
	vector<bool> *cstr;//指向一维容器数组的指针,数组的元素是vector容器

public:
	vector<string> wordBreak(string s, unordered_set<string> &dict) {
		//构建子字符串对应的bool数组来存储信息,true表示该子串在字典中存在
		cstr = new vector<bool>[s.length()];

		for (int i = 0; i < s.length(); i++) {
			for (int j = i; j < s.length(); j++) {
				if (dict.end() != dict.find(s.substr(i, j - i + 1))) {
					cstr[i].push_back(true);
				}
				else {
					cstr[i].push_back(false);
				}
			}
		}
		//将字符串按照数字存储内容分割
		handleStr(s.size() - 1, s);
		return result;
	}

	void handleStr(int i, string s) {//字符串s和数组查找序号
		if (i == -1) {//递归终止条件
			string str;
			for (int j = mystring.size() - 1; j >= 0; j--) {
				str += mystring[j];
				if (j != 0)
					str += " ";
			}
			result.push_back(str);
		}
		else {
			for (int k = i; k >= 0; k--) {//自底向上自左向右遍历数组
				if (cstr[k][i - k]) {
					mystring.push_back(s.substr(k, i - k + 1));
					handleStr(k - 1, s);
					mystring.pop_back();
				}
			}
		}
	}
};

2、candy

(1)题目描述
有N个小朋友站在一排,每个小朋友都有一个评分
你现在要按以下的规则给孩子们分糖果:

  • 每个小朋友至少要分得一颗糖果
  • 分数高的小朋友要他比旁边得分低的小朋友分得的糖果多

你最少要分发多少颗糖果?

There are N children standing in a line. Each child is assigned a
rating value. You are giving candies to these children subjected to
the following requirements:

Each child must have at least one candy. Children with a higher rating
get more candies than their neighbors. What is the minimum candies you
must give?

(2)思路分析
假设n个孩子的评分如下:

学生编号0123456
ratings[i]5143326

有i个学生时的糖果数如下图:

candys[i]
i=01
i=121
i=2212
i=32121
i=421211
i=5213221
i=62132212
  1. 与前面的邻居比较,前向遍历权重数组ratings,如果ratings[i]>ratings[i-1],则cands[i]=cands[i-1]+1;
  2. 与后面的邻居比较,后向遍历权重数组ratings,如果ratings[i-1]>ratings[i]且cands[i]>=cands[i-1],则更新cands,cands[i]=cands[i+1]+1;
  3. 对cands求和即为最少需要的糖果。

(3)牛客网线上测试代码以及注释:

class Solution {
public:
	int candy(vector<int> &ratings) {
		//特殊值处理
		int len = ratings.size();
		if (len == 0)
			return 0;
		if (len == 1)
			return 1;

		//每个孩子初始化 有1个糖果
		vector<int> candys(len, 1);
		//从左向右扫描  与前比
		for (int i = 1; i < len; i++) {
			if (ratings[i] > ratings[i - 1])
				candys[i] = candys[i - 1] + 1;
		}
		//从右向左扫描 与后比
		for (int i = len - 1; i > 0; i--) {
			if (ratings[i] < ratings[i - 1] && candys[i] >= candys[i - 1]) {
				candys[i - 1] = candys[i] + 1;
			}
		}
		
		//计算最优值
		int sum = 0;
		for (int i = 0; i < len; i++) {
			sum += candys[i];
		}

		return sum;
	}
};

3、palindrome-partitioning-ii

(1)题目描述
给出一个字符串s,分割s使得分割出的每一个子串都是回文串,计算将字符串s分割成回文分割结果的最小切割数。

例如:给定字符串s=“aab”,
返回1,因为回文分割结果[“aa”,“b”]是切割一次生成的。

Given a string s, partition s such that every substring of the
partition is a palindrome. Return the minimum cuts needed
for a palindrome partitioning of s.

For example, given s =“aab”, Return 1 since the palindrome partitioning[“aa”,“b”]could be produced using 1 cut.

(2)思路分析
定义动态规划数组dp,dp[i]的含义是子串str[0…i]至少需要切割几次,才能把str[0…i]全部切成回文子串,那么dp[len-1]就是最后的结果。
从左往右依次计算dp[i]的值,i 初始为0,具体计算过程如下:

A、假设 j 处在 0 到 i 之间,如果str[j…i]是回文串,那么dp[i]的值可能是dp[j-1] + 1,其含义是在str[0…i]上,既然str[j…i]是回文串,那么它可以自己作为一个分割的部分,剩下的部分str[0…j-1]继续做最经济的分割,也就是dp[j-1]的值。


B、根据步骤A的方式,让 j 在 i 到 0 的位置上枚举,那么所有可能中最小值就是dp[i]的值,
即dp[i] = min{dp[j-1]+1 (0<= j <= i,且str[j…i]必须是回文串)}。

C、如何快速方便的判断str[j…i]是否为回文串?

1)定义一个二维数组p,如果p[j][i]为True,表示str[j…i]是回文串,否则不是。在计算dp过程中,希望能够同步、快速的计算出矩阵p。

2)p[j][i]如果为True,一定来自以下三种情况:

  • str[j][i]由一个字符组成
  • str[j][i]由两个字符组成且两个字符相等
  • str[j][i]由多个字符组成,str[j] == str[i]且p[j+1][i-1] == True。

3)在计算dp数组的过程中,位置i是从左向右依次计算的。而对于每一个i来说,又依次从 i 位置向左遍历所有的位置,以此来决策dp[i]。所以对于p[j][i]来说,p[j+1][i-1]一定已经计算过。
(3)牛客网测试代码

class Solution
{
public:
	int minCut(string s)
	{
		int len = s.size();
		vector<int> vec(len, 0);//vec[i]存储str[0...i]的最少回文串数目
		vector< vector<bool> > bvec(len, vector<bool>(len, false));//判断str[j..i]是否是回文串

		for (int i = 0; i < len; i++)
		{
			vec[i] = i;
			for (int j = i; j >= 0; j--)
			{
				if ((s[i] == s[j]) && (i - j < 2 || bvec[i - 1][j + 1]))//子串str[j...i]是回文串的判断条件
				{
					bvec[i][j] = true;
					
					if (j == 0)
						vec[i] = min(vec[i], 0);
					else
						vec[i] = min(vec[i], vec[j - 1] + 1);
				}
			}
		}
};

-----------------------------------------------------------------------------------------------------------------------------------------------------
如果本文对你有所帮助,请不要忘了点赞、收藏哦!!!
-----------------------------------------------------------------------------------------------------------------------------------------------------

三、相关参考

参考一:https://www.cnblogs.com/hithongming/p/9229871.html
参考二:https://blog.csdn.net/i_am_bird/article/details/78153666?utm_source=blogxgwz0
参考三:https://blog.csdn.net/xinwenhuayu/article/details/100899478

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值