第四节课:


题目一

在这里插入图片描述
个人认为,本题左神给的方法比较麻烦,顾不采用。以下是笔者给出的解法:首先对数组排序;双指针l、r分别指向数组的0位置和最后位置;如果两指针指向位置的元素之和小于limit,这个两个人可以乘坐同一条船–>l++,r–,结果加一;否则r指向的位置的元素只能独自乘坐一条船(因为最小的数都不能和他同坐一条船)–>r–,l不动,结果加一;直到l>r。
代码实现:

int minBoat(vector<int>& arr,int limit) {
	if (arr.size() == 0) {
		return 0;
	}
	sort(arr.begin(), arr.end());
	if (arr[arr.size() - 1] > limit) {
		return INT_MAX;
	}
	int l = 0;
	int r = arr.size() - 1;
	int res = 0;
	while (l <= r) {
		if (arr[r] + arr[l] > limit) {
			res++;
			r--;
		}
		else {
			res++;
			r--;
			l++;
		}
	}
	return res;
}


题目二

在这里插入图片描述
方法一:最长公共子序列问题解决之后,把字符串和他自己的逆序串,去求最长公共子序列,就代表原始串的最长回文子序列。
常见的尝试模型:从左往右尝试(从左往右,i位置上的数要还是不要)、范围上尝试、两个字符串对应。
范围上尝试模型:s[i…j]范围上最长回文子序列长度多少(加粗部分为判断标准,不同题目标准不同);参数的变化范围是正方形,但是左下半区无用(不包括对角线,对角线上的值很好确定,此对角线上也是,basecase)。范围上尝试模型要重点讨论开头和结尾,以开头结尾进行可能性分类。范围上尝试模型在这种可能性分类下,能否解决问题。一般是求子序列问题,但其他问题也可以用。
本题:对角线上的值全为1(单个字符的最长回文子序列必是1),此对角线上的值2或者1(两个字符相等为2,否则为0);根据开头结尾讨论:形成的最长回文子序列,1) 不以i开头、不以j结尾–>dp[i][j]=dp[i+1][j-1];2) 以i开头、不以j结尾–>dp[i][j]=dp[i][j-1];3) 不以i开头、以j结尾–>dp[i][j]=dp[i+1][j];4) 以i开头、以j结尾–>dp[i][j]=dp[i+1][j-1]+2,但是这种情况有前提条件,s[i]==s[j];四种情况取最大值。从下往上,从左往右填表。

在这里插入图片描述
代码实现:

//方法一:原串和他的逆序串的最长公共子序列就是原串的最长回文子序列
//dp[i][j]:s1[0..i]和s2[0..j]最长公共子序列的长度
int maxLenSubStr(string& s1, string& s2) {
	vector<vector<int>>dp(s1.length(), vector<int>(s2.length()));
	dp[0][0] = s1[0] == s2[0] ? 1 : 0;
	for (int i = 1; i < s1.length(); i++) {
		dp[i][0] = (dp[i - 1][0] == 1 || s1[i] == s2[0] ? 1 : 0);
	}
	for (int j = 1; j < s2.length(); j++) {
		dp[0][j] = (dp[0][j - 1] == 1 || s1[0] == s2[j] ? 1 : 0);
	}
	for (int i = 1; i < s1.length(); i++) {
		for (int j = 1; j < s2.length(); j++) {
			dp[i][j] = dp[i - 1][j - 1] + (s1[i] == s2[j] ? 1 : 0);
			dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i][j - 1]));
		}
	}
	return dp[s1.length() - 1][s2.length() - 1];
}
int maxLen(string& s) {
	if (s.length() == 0) {
		return 0;
	}
	string s_re = s;
	reverse(s_re.begin(), s_re.end());
	return maxLenSubStr(s, s_re);
}

//方法二:范围上尝试
//dp[i][j]:s[i..j]范围上最长回文子序列
int maxLen2(string& s) {
	if (s.length() == 0) {
		return 0;
	}
	vector<vector<int>>dp(s.length(), vector<int>(s.length()));
	for (int i = 0; i < s.length(); i++) {
		dp[i][i] = 1;
	}
	for (int i = 0; i < s.length() - 1; i++) {
		dp[i][i + 1] = s[i] == s[i + 1] ? 2 : 1;
	}
	for (int i = s.length() - 3; i >= 0; i--) {
		for (int j = i + 2; j < s.length(); j++) {
			dp[i][j] = dp[i + 1][j - 1] + (s[i] == s[j] ? 2 : 0);//三目运算符要加括号
			dp[i][j] = max(dp[i][j], max(dp[i + 1][j], dp[i][j - 1]));
		}
	}
	return dp[0][s.length() - 1];
}

题目三

在这里插入图片描述
首先解决至少添加几个字符可以让整体变成回文字符串:范围上尝试,s[i…j]范围内至少添加几个字符串可以让他变成回文字符串。对角线位置为0,此对角线位置,相邻的两字符相同为0,不同为1;一般位置有三种可能性:1 先搞定s[i+1…j],最后再搞定i位置–>dp[i][j]=dp[i+1][j]+1;2 先搞定s[i…j-1],最后再搞定j位置–>dp[i][j]=dp[i][j-1]+1;3 在s[i]==s[j]的前提条件下,搞定中间部分就可以了–>dp[i][j]=dp[i+1][j-1],三种情况取最小值。

在这里插入图片描述
怎么利用填好的动态规划表来还原出添加最少字符的情况下得到的回文字符串的一个结果?首先准备一个长度为最终回文字符串长度的字符串;从表的右上角开始沿着填表的路径来填这个字符串,直到来到表的对角线位置,停止填写字符串。如果要求所有的结果,用深度优先遍历。

在这里插入图片描述

代码实现:

//dp[i][j]:在s[i..j]范围内至少插入多少字符,使s[i..j]变成回文字符串
string toPalindrome(string& s) {
	if (s.length() < 2) {
		return s;
	}
	int len = s.length();
	vector<vector<int>>dp(len, vector<int>(len));
	for (int i = 0; i < len - 1; i++) {
		dp[i][i + 1] = (s[i] == s[i + 1] ? 0 : 1);
	}
	//从下往上,从左往右填表
	for (int i = len - 3; i >= 0; i--) {
		for (int j = i + 2; j < len; j++) {
			dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
			if (s[i] == s[j]) {
				dp[i][j] = min(dp[i][j], dp[i + 1][j - 1]);
			}
		}
	}

	string res(len + dp[0][len - 1], 'a');
	int len_res = res.length();
	int i = 0;
	int j = len - 1;
	int k = 0;//填res的位置
	while (i <= j) {
		if (s[i] == s[j]&& dp[i + 1][j - 1] < min(dp[i][j - 1], dp[i + 1][j])) {
			res[k] = s[i];
			res[len_res - k - 1] = res[k];
			k++;
			i = i + 1;
			j = j - 1;
		}
		else {
			if (dp[i][j - 1] > dp[i + 1][j]) {
				res[k] = s[i];
				res[len_res - k - 1] = res[k];
				k++;
				i = i + 1;
				j = j;

			}
			else {
				res[k] = s[j];
				res[len_res - k - 1] = res[k];
				k++;
				i = i;
				j = j - 1;
			}
		}
	}
	return res;
}

题目四

在这里插入图片描述
f(i):s[i…]全部切成回文子串的最少分割数;f(i)等于s[i]自己成为回文+f(i-1)、s[i.i+1]成为回文+f(i-2)…s[i…end]成为回文,以上所有情况求最小值,每种情况存在的条件是s[i…j]自己可以成为回文。

在这里插入图片描述
i从0到最后遍历整个字符串,每到一个位置又要遍历在其后面的若干字符组成回文的情况下,最少的分割数,大的结构的时间复杂度为O(N2),同时判断i及其之后的若干字符串能否组成回文的时间复杂度为O(N),因此整体复杂度为O(N3)!!!怎么优化呢?可以预处理出一张s[i…j]范围是否是回文的表,每次需要判断s[i…j]范围是否为回文时,直接查表。时间复杂度可以优化为O(N^2)。

在这里插入图片描述
代码实现:

//判断s[i..j]是否为回文
//时间复杂度:O(N)
bool isPalindrome(string& s, int i, int j) {
	if (i == j) {
		return true;
	}
	if (j == i + 1) {
		return s[i] == s[j] ? true : false;
	}
	return s[i] == s[j] && isPalindrome(s, i + 1, j - 1);
}

//返回s[i..]分割成回文的最少分割数
//时间复杂度:O(N^3)
int f(string& s, int i) {
	if (i==s.length()) {
		return 0;
	}
	int res = INT_MAX;
	
	//以s[i..ed]为第一个回文,的最少回文数,遍历ed,求所有情况的最小值
	for (int ed = i; ed < s.length(); ed++) {
		if (isPalindrome(s, i, ed)) {
			res = min(res, f(s, ed + 1) + 1);
		}
	}
	return res;
}


//改动态规划
//dp[i][j]:s[i..j]范围是否为回文
vector<vector<bool>> isPalindrome2(string& s) {
	int len = s.length();
	vector<vector<bool>>dp(len, vector<bool>(len));
	for (int i = 0; i < len; i++) {
		dp[i][i] = true;
	}
	for (int i = 0; i < len - 1; i++) {
		dp[i][i + 1] = (s[i] == s[i + 1] ? true : false);
	}
	//从下往上,从左往右
	for (int i = len - 3; i >= 0; i--) {
		for (int j = i + 2; j < len; j++) {
			dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]);
		}
	}
	return dp;
}

//dp[i]:s[i..]分割成回文的最少分割数
//时间复杂度:O(N^2)
int f2(string& s) {
	int len = s.length();
	if (len < 2) {
		return 0;
	}
	vector<vector<bool>>isPalind = isPalindrome2(s);
	vector<int>dp(len+1,INT_MAX);
	dp[len] = 0;
	for (int i = len - 1; i >= 0; i--) {
		for (int ed = i; ed < s.length(); ed++) {
			if (isPalind[i][ed]) {
				dp[i] = min(dp[i], dp[ed + 1] + 1);
			}
		}
	}
	return dp[0];
}

题目五

在这里插入图片描述
移除和保留是同一个问题,下面考虑怎么保留:
在这里插入图片描述
在这里插入图片描述
dp[i][j]:s[i…j]范围上有多少中保留方案。本题最难的是可能性整理,下面着重讨论:
我们想要的dp[i][j]的结果是,1) 必须以i开头、必须以j结尾;2) 必须以i开头、必须不以j结尾;3) 必须不以i开头、必须以j结尾;4) 必须不以i开头、必须不以j结尾,这四种互斥且相加为全量的可能结果求和。但是,(a) dp[i+1]dp[j]表示:s[i+1…j]范围上有多少中保留方案,这其中可以选择要j或者不要j,即包含了3)和4)两种情况;(b) dp[i]dp[j-1]表示:s[i…j-1]范围上有多少中保留方案,这其中可以选择要i或者不要i,即包含了2)和4)两种情况;© dp[i+1]dp[j-1]表示:s[i+1…j-1]范围上有多少中保留方案,只能既不要i又不要j,即包含了4)这种情况;(d)在s[i]==s[j]成立的前提下,dp[i+1]dp[j-1]+1,即为1)。所以,我们想要的:
dp[i][j]=1)+2)+3)+4)=(d)+(a)+(b)-(c )
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

代码实现:

//dp[i][j]:s[i..j]范围上变成回文,有多少保留方案
int reserveWays(string& s) {
	if (s.length() == 0) {
		return 0;
	}
	int len = s.length();
	vector<vector<int>>dp(len, vector<int>(len));
	for (int i = 0; i < len; i++) {
		dp[i][i] = 1;
	}
	for (int i = 0; i < len - 1; i++) {
		dp[i][i + 1] = 2 + (s[i] == s[i + 1] ? 1 : 0);
	}
	//从下往上,从左往右
	for (int i = len - 3; i >= 0; i--) {
		for (int j = i + 2; j < len; j++) {
			dp[i][j] = dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1];
			if (s[i] == s[j]) {
				dp[i][j] += dp[i + 1][j - 1] + 1;
			}
		}
	}
	return dp[0][len - 1];
}


题目六

在这里插入图片描述

方法一:利用快排的patition,判断等于区域有没有命中第k小,如果命中就可以直接返回,如果没命中,判断是在大于区域还是在小于区域,在选定区域在进行patition,直到命中。
利用master公式求时间复杂度:O(N)=0。利用随机选取一个值作为patition的划分值,利用概率达到了时间复杂度O(N)。下面介绍不用概率,达到严格O(N)的算法,BFPRT算法

在这里插入图片描述
方法二:BFPRT算法。整体思路和方法一一样,不同点在于,BFPRT算法不是随机选一个数,而是有讲究的选一个数,后续过程一样。

在这里插入图片描述

怎么有讲究的选一个数?假设求无序数组中第k小的数的函数为f(arr,k);首先每五个数分一组,最后一组可以不够五个数;每一组中求中位数(上中位数),组成一个数组marr,这一步的时间复杂度为O(N);递归调用f函数,求marr的中位数(上中位数),即f(marr,marr的长度/2),假设返回值为x,那么x就是有讲究的选的那个数。

在这里插入图片描述
为什么时间复杂度是O(N)呢?方法一不能确定选出的数左右侧是什么规模,如果很偏,则时间复杂的就是变大,而方法二可以控制左侧/右侧最差的规模。假设没有命中,并且要去左侧递归,左侧最差规模就是小于x的数最多有多少个,为了方便估计,转化为估计大于等于x的数至少有多少个。marr的规模是N/5,选出的x又是marr的中位数,所以marr中至少有N/10的数是大于等于x的,x大于等于其他组的中位数对应的组,又至少2的数是大于等于其中位数的,所以大于等于x的数至少又3N/10个,那么小于x的数最多又7N/10个。
在这里插入图片描述
总的时间复杂度分析:
在这里插入图片描述
代码实现:

//方法一:利用快排的partition
int partion(vector<int>& arr,int l,int r,int k) {
	int less = l - 1;
	int more = r;
	int rd = rand() % (r - l + 1) + l;
	swap(arr[r], arr[rd]);
	int i = l;
	int num = arr[r];
	while (i < more) {
		if (arr[i] > num) {
			swap(arr[i], arr[more - 1]);
			more--;
		}
		else if (arr[i]<num) {
			swap(arr[i], arr[less + 1]);
			less++;
			i++;
		}
		else {
			i++;
		}
	}
	swap(arr[r], arr[more]);
	more++;
	if (k > less && k < more) {
		return num;
	}
	if (k <= less) {
		return partion(arr, l, less, k);
	}
	return partion(arr, more, r, k);
}


//方法二:BFPRT算法
int getMedian(vector<int> arr, int l, int r) {
	//l-l有序
	//想要l-l+1有序
	for (int i = l+1; i <= r; i++) {
		for (int j = i - 1; j >= l&&arr[j]>arr[j+1]; j--) {
			swap(arr[j], arr[j+1]);
		}
	}
	return arr[l + ((r - l) >> 1)];
}

//获取中位数的中位数--有讲究的选的划分值
int bfprt(vector<int>& arr, int l, int r, int k);
int medianOfMedians(vector<int>& arr, int l, int r) {
	int len = r - l + 1;
	int M = len / 5 + (len % 5 == 0 ? 0 : 1);//一共又M组
	vector<int>marr(M);
	for (int i = 0; i < M; i++) {
		int start = l + 5 * i;
		int ed = min(r, start + 4);
		marr[i] = getMedian(arr, start, ed);
	}

	return bfprt(marr, 0, M - 1, M / 2);
}

vector<int>partion2(vector<int>& arr, int l, int r, int pivot) {
	int less = l - 1;//小于区域的右边界
	int more = r + 1;//大于区域的左边界
	int i = l;
	while (i < more) {
		if (arr[i] > pivot) {
			swap(arr[i], arr[more - 1]);
			more--;
		}
		else if (arr[i] < pivot) {
			swap(arr[i], arr[less + 1]);
			less++;
			i++;
		}
		else {
			i++;
		}
	}
	return vector<int>({ less + 1,more - 1 });
}

//在arr[l..r]范围内,求如果排序的话,k位置的数是谁,返回
//k一定在l..r范围内
int bfprt(vector<int>& arr, int l, int r, int k) {
	if (l == r) {
		return arr[l];
	}
	//分组+组内排序+各组的中位数组成marr+返回marr的中位数
	int pivot = medianOfMedians(arr, l, r);

	//根据pivot做划分值,<	=	>
	//返回等于区域的左右边界
	vector<int>pivotRange = partion2(arr, l, r, pivot);
	if (k >= pivotRange[0] && k <= pivotRange[1]) {
		return arr[k];
	}
	if (k < pivotRange[0]) {
		return bfprt(arr, l, pivotRange[0] - 1, k);
	}
	return bfprt(arr, pivotRange[1] + 1, r, k);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值