c++动态规划类算法编程汇总(二)全排列| O(n)排序 | manacher法 |滑窗|最长回文串

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

c++动态规划类算法编程汇总(一)背包问题|回溯法

c++动态规划类算法编程汇总(二)全排列| O(n)排序 | manacher法

c++策略类O(n)编程问题汇总(扑克的顺子|约瑟夫环|整数1出现的次数|股票最大利润)

目录

一、加油站与油O(n)

1.1 思路

1.2 解法

二、01矩阵中到0的最小步数

2.1 思路

三、不用冒泡的稳定O(n)

扫描二维码关注公众号,回复: 9391616 查看本文章

3.1 思路

四、滑动窗口最大值

4.1 要求O(n)的算法复杂度

4.2 判断语句的执行问题

4.3 段错误来自什么地方?

五、类似排序的奇偶排序

5.1 解法

5.2 输入乱序链表

六、全排列

6.1 非最佳方案

6.2 最终方案

七、最长回文串

7.1 题干

7.2 暴力解法

7.3 动态规划

7.4 插入#简化映射

7.5 manacher法


一、加油站与油O(n)

leet code 134:oj:

https://leetcode-cn.com/problems/gas-station/submissions/

问题:共n个加油站,123456....n个加油站,每个站点 i 能加 add[ i ] 升汽油, 但是到下一个站点需要花费 sub[ i ] 升汽油。只能从一个站点 i 到下一个站点 i+1 ,从n 到 1,是一个环状的路程,但是不能往回走。问从哪里出发能走完全程?

1.1 思路

问题转换

先把每个站点构造数列 score[ i ] = add[ i ] - sub[ i ],

这个问题就转换成环状的 score [i]  从哪个位置出发,可以实现他们的和 大于0

Sum(score)>0则可以实现,证明:全局>0则局部必然存在>0。问题是在于找出局部在哪里?从哪里开始

解法

选择score最大的节点,两个指针之间,一个fast,一个slow,从前往后加入sum,大于零则继续往后加,小于则用下一个节点加,往前加。如果可以使得sum>0且两指针重合,则满足。

1.2 解法

按照如上思路编写程序。

  • 只要输入变量不加const修饰,可直接用相应的输入的变量存储中间结果节省运算,例如直接用cost=gas-cost
  • 用 fast%length来实现相应的取地址操作和环形的循环操作
  • 第一个循环中判断,slow == fast - 1 && slow<length-1,要用length-1来防止循环溢出,或者用slow == fast - 1 && fast<length
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

class Solution {
public:
	int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
		int length = gas.size();
		if (length < 1 || length!=cost.size())return -1;
		for (int idx = 0; idx < length; idx++){
			cost[idx] = gas[idx] - cost[idx];
		}
		int fast=1; int slow=0;
		int sum = cost[0];
		for (slow = 0; slow <  length; slow++){
			while (sum < 0 && slow == fast - 1 && slow<length-1){
				slow++;
				fast++;
				sum = cost[slow];
			}
			while (sum >= 0){
				if (fast>slow && fast%length == slow){
					return slow;
				}
				sum += cost[fast%length];
				fast++;
			}

			sum -= cost[slow];
		}
		return -1;
	}
};

int main(){
	vector<int>gas = { 1, 2, 3, 4, 5 };
	vector<int>cost = { 3, 4, 5, 1, 2};
	Solution s1;
	cout << s1.canCompleteCircuit(gas, cost) << endl;

	//cout << s1.minPathSum(grid) << endl;

	int end; cin >> end;
	return 0;
}

二、01矩阵中到0的最小步数

问题:输入一个0,1矩阵,比如

0 0 0 1
1 0 1 1 
1 1 1 1 
0 0 1 0

问矩阵中每个位置到最近的0的曼哈顿距离(只能上下左右走,走到0的步数)。比如此题答案就是

2.1 思路

笨方法:

遍历所有的距离0距离是1的位置,填入1,

然后遍历所有距离1距离是1并且没有填过的位置填入2,依次类推,直到最长边m,但此算法复杂度高,需要O(m*mn),mn为矩阵大小。

动态规划方法

从左上到右下和从右下到左上分别遍历两次。

左上到右下的遍历就是,当前到来自左上方0的距离为:左块和上块最小值加1   current_distance=min(left_distance, up_distance)+1

右下到左上的遍历类推。最终的矩阵为  min(左上距离,右下距离)

OJ与程序待补充。

三、不用冒泡的稳定O(n)

1 2 3 5 0 5 6 2 4 0 0 0 0 0 5

如何将序列的0移到最后,且不变换非0值的顺序。算法复杂度O(n)

3.1 思路

不可行方案:

原始思路就像冒泡排序那样,不可取,因为复杂度O(n*n)

快速排序不可取,因为快速排序是不稳定排序,打算非零值的顺序。

正确思路:

先遍历一次,找出非零值的数量(这步可以省略)。

然后两个指针,一个fast,一个slow,一起往下遍历。fast移一次只能指向非0值,slow只能从前往后遍历

每次把fast的值填入slow,当fast到末尾的时候,slow后面的值置0

OJ与程序待补充。

四、滑动窗口最大值

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

OJ:https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=4&rp=4&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

4.1 要求O(n)的算法复杂度

采用方法:https://cuijiahua.com/blog/2018/02/basis_64.html

错误写法:

此题本地可以正常运行,但是到了OJ就总显示段错误。

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

class Solution {
public:
	vector<int> maxInWindows(const vector<int>& num, unsigned int size)
	{
		int length = num.size(); 
		vector<int> max_value;
		if (length < 1 || length < size || length < 1)return max_value;
		deque<int> buffer;
		for (int idx = 0; idx < size; idx++){
			while (!buffer.empty() ){
				if (num[idx] >= num[buffer[buffer.size() - 1]])
					buffer.pop_back();
				else
					break;
			}
			buffer.push_back(idx);
		}
		for (int idx = size; idx < length; idx++){
			max_value.push_back(num[buffer[0]]);
			while (!buffer.empty()  ){
				if (num[idx] >= num[buffer[buffer.size() - 1]]){
					if (num[idx] >= num[buffer[buffer.size() - 1]])
						buffer.pop_back();
					else
						break;
				}
				else
					break;
				
			}
			buffer.push_back(idx);
			while (idx-size+1>buffer[0]){
				buffer.pop_front();
			}
		}
		max_value.push_back(num[buffer[0]]);
		return max_value;
	}
};
int main(){
	vector<int> test = { 2, 3, 4, 2, 6, 2, 5, 4,3,2,1,0 };
	Solution s1;
	int window_size = 3;
	vector<int> result = s1.maxInWindows(test, window_size);
	//input 
	for (auto item : test){
		cout << item << " ";
	}
	cout << endl;
	// output
	for (int idx = 0; idx < window_size - 1; idx++){
		cout << "  ";
	}
	for (auto item : result){
		cout << item << " ";
	}
	cout << endl;
	int end; cin >> end;
	return 0;
}

问题描述:

您的代码已保存
段错误:您的程序发生段错误,可能是数组越界,堆栈溢出(比如,递归调用层数太多)等情况引起
case通过率为0.00%

这里我们需要弄明白几个问题:

  • 判断中,A&&B,如果第一个A语句为假了,B语句是否会执行?
  • 判断中, A||B,如果第一个A为真了,第二个B语句是否会执行?
  • 段错误到底来自于什么?

4.2 判断语句的执行问题

答案是,如果第一个语句可以完成判断,则第二个语句不被执行。例如:

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

bool print1(){
	cout << "Whats your problem?11111" << endl;
	return true;
}
bool print2(){
	cout << "Whats your problem?22222" << endl;
	return true;
}
int main(){
	
	if (print1() || print2())
		cout << "yes,next" << endl;

	bool pr1 = print1();
	bool pr2 = print2();
	if (pr1&&pr2)
		cout << "verfied" << endl;
	int end; cin >> end;
	return 0;
}
/* 输出
Whats your problem?11111
yes,next
Whats your problem?11111
Whats your problem?22222
verfied
*/

4.3 段错误来自什么地方?

为什么本地IDE可以运行但是服务器就显示段错误?

待检查

五、类似排序的奇偶排序

第一题:输入乱序数组,使奇数值排在偶数值之前(要求O(n)时间复杂度,O(1)空间复杂度)

第二题:输入乱序链表,使奇数值排在偶数值之前(要求O(n)时间复杂度,O(1)空间复杂度)

5.1 解法

输入乱序数组

作者:offer从天上来
链接:https://www.nowcoder.com/discuss/226064?type=post&order=time&pos=&page=1
来源:牛客网

#include<iostream>
#include<vector>
 
using namespace std;
 
//乱序数组,使奇数值排在偶数值之前(要求O(n)时间复杂度,O(1)空间复杂度)
void func(vector<int> &array)
{
    if (array.size() < 2)
        return;
    int start = 0, end = array.size() - 1;
    while (start < end)
    {
        while (array[start] & 0x0001)
        {
            if (start == end)
                break;
            ++start;
        }
        while ((array[end] & 0x0001) == 0)
        {
            if (end == start)
                break;
            --end;
        }
        if (start == end)
            break;
        int temp = array[start];
        array[start] = array[end];
        array[end] = temp;
        ++start;
        --end;
    }
}
 
int main()
{
    int n;
    while (cin >> n)
    {
        vector<int> input;
        int temp;
        for (int i = 0; i < n; ++i)
        {
            cin >> temp;
            input.push_back(temp);
        }
        func(input);
        for (auto it : input)
            cout << it << ' ';
        cout << endl;
    }
    return 0;
}

5.2 输入乱序链表

作者:offer从天上来
链接:https://www.nowcoder.com/discuss/226064?type=post&order=time&pos=&page=1
来源:牛客网

#include<iostream>
#include<vector>
 
using namespace std;
 
//乱序链表,使奇数值排在偶数值之前(要求O(n)时间复杂度,O(1)空间复杂度)
struct ListNode{
    int val;
    ListNode* next;
    ListNode(int x) :val(x), next(NULL){}
};
 
void func(ListNode** root)
{
    if (root == NULL)
        return;
    ListNode* pNode = *root;
    ListNode* preNode = *root;
 
    pNode = pNode->next;
    while (pNode)
    {
        if (pNode->val & 0x0001)
        {
            preNode->next = pNode->next;
            pNode->next = *root;
            *root = pNode;
            pNode = preNode->next;
        }
        else
        {
            preNode = pNode;
            pNode = pNode->next;
        }
    }
}
ListNode* constructList(const vector<int> &array)
{
    if (array.size() == 0)
        return NULL;
    ListNode* root = new ListNode(array[0]);
    ListNode* pNode = root;
    for (int i = 1; i < array.size(); ++i)
    {
        pNode->next = new ListNode(array[i]);
        pNode = pNode->next;
    }
    return root;
}
 
void printList(ListNode* root)
{
    ListNode* pNode = root;
    while (pNode)
    {
        cout << pNode->val << ' ';
        pNode = pNode->next;
    }
    cout << endl;
}
int main()
{
    int n;
    while (cin >> n)
    {
        vector<int> input;
        int temp;
        for (int i = 0; i < n; ++i)
        {
            cin >> temp;
            input.push_back(temp);
        }
        ListNode* root = constructList(input);
        func(&root);
        printList(root);
    }
    return 0;
}

六、全排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cabcba

思路要清晰,

  • 每个节点idx与后面所有的互换,
  • 然后往下递归,将idx+1与后面互换。
  • 互换到idx=最后一个(size-1)的时候,将结果存入set

6.1 非最佳方案

此方法算法复杂度较高,并且思路不太对:

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

class Solution {
public:
	vector<string> Permutation(string str) {
		loc_Permutation(str, 0);
		vector<string> result;
		if (str.size() == 0)return result;
		for (auto item : all_str){
			result.push_back(item);
		}
		return result;
	}
	void loc_Permutation(string str, int loc){
		all_str.insert(str);
		int size = str.size();
		if (loc == size - 1)return;
		for (int idx = loc; idx < size-1; idx++){
			for (int idx_swap = idx + 1; idx_swap < size; idx_swap++){
				swap(str[idx],str[idx_swap]);
				loc_Permutation(str, loc + 1);
				swap(str[idx], str[idx_swap]);
			}
		}
	}
public:
	set<string> all_str;
};

int main(){
	string a = "123";
	Solution s1;
	for (auto item : s1.Permutation(a)){
		cout << item << endl;
	}
	int end; cin >> end;
	return 0;
}
  1. 注意一点,set头文件可以不重复的输入进去,当作集合,用insert函数。对于aa,输出只有aa,而不是[aa,aa],所以必须用set
  2. 编程序的时候,涉及到下标的,要画出来具体化。
  3. class中可以设置全局变量
                            swap(str[idx],str[idx_swap]);
                            loc_Permutation(str, loc + 1);
                            swap(str[idx], str[idx_swap]);

运用loc+1的时候

遍历的完整性与不重不漏:

例如:可以在每个void函数后面输出当前str,输出当前idx,与idx_swap

123
0 1 213
1 2 231
0 2 321
1 2 312
1 2 132
1 2 123

可以看作程序如此运行

123

 循环中换位置 213  递归231

 循环中换位置 321  递归312

 循环中换位置 132  递归123(此步导致重复)

此处可以重新改进程序,即当前位置交换之后,即可swap后面改为idx+1也可以

              for (int idx = loc; idx < size - 1; idx++){
                     for (int idx_swap = idx + 1; idx_swap < size; idx_swap++){
                            //cout << idx << " " << idx_swap << " ";
                            swap(str[idx], str[idx_swap]);
                            loc_Permutation(str, idx + 1);
                            swap(str[idx], str[idx_swap]);
                     }
              }

但是必须用set

6.2 最终方案

class Solution {
public:
	vector<string> Permutation(string str) {
		loc_Permutation(str, 0);
		vector<string> result;
		if (str.size() == 0)return result;
		for (auto item : all_str){
			result.push_back(item);
		}
		//result = all_str;
		return result;
	}
	void loc_Permutation(string str, int loc){
		all_str.insert(str);
		//all_str.push_back(str);
		//cout << str << endl;
		int size = str.size();
		if (loc == size - 1)return;
		//loc_Permutation(str, loc + 1);
		for (int idx_swap = loc ; idx_swap < size; idx_swap++){
			//cout << loc << " " << idx_swap << " ";
			swap(str[loc], str[idx_swap]);
			loc_Permutation(str, loc + 1);
			swap(str[loc], str[idx_swap]);
		}
		
	}
public:
	set<string> all_str;
};

 

七、最长回文串

7.1 题干

Leetcode 5

Leetcode5

https://leetcode-cn.com/problems/longest-palindromic-substring/

暴力求解方法可以先做:

输入: "babad"

输出: "bab"

注意: "aba" 也是一个有效答案。

输入: "cbbd"

输出: "bb"

需要注意,substr函数是这样用的,string.substr(初始位置,字串长度)

不可行,总是存在算法复杂度过高的问题,本题算法复杂度O(N*N*N)

7.2 暴力解法

这种算法复杂度 O(N^3)的显然不可以。OJ也不会通过

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

class Solution {
public:
	bool if_reverse(string s){
		int len = s.size();
		for (int idx = 0; idx <= (len / 2); idx++){
			if (s[idx] != s[len - idx - 1]){
				return false;
			}
		}
		return true;
	}
	string longestPalindrome(string s) {
		int str_size = s.size();
		string max_str = s.substr(0, 1);
		int max_length = 1;
		for (int start_loc = 0; start_loc < str_size - max_length; start_loc++){
			for (int sub_size = str_size - start_loc; sub_size>max_length; sub_size--){
				string sub = s.substr(start_loc, sub_size);
				if (if_reverse(sub) && sub_size>max_length){
					max_str = sub;
					max_length = sub_size;
				}
			}
		}
		return max_str;
	}
};

int main(){
	//string A; cin >> A;
	string A = "babad";
	string B = "cbbd";
	Solution Solution;
	cout << A << endl;
	cout << Solution.longestPalindrome(A) << endl;
	cout << B << endl;
	cout << Solution.longestPalindrome(B) << endl;
	int end; cin >> end;
	return 0;
}

7.3 动态规划

动态规划的算法复杂度为O(N^2),即当前节点回文,则(节点最左往左==节点最左往右)这两个转换条件可以达到下一个节点回文。算法复杂度依然较高,但是此时已经可以通过OJ的测试了。

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

class Solution {
public:
	string longestPalindrome(string s) {
		int str_size = s.size();
		int location = 0;
		int max_length = 0;
		//odd size
		for (int loc_idx = 0; loc_idx < str_size; loc_idx++){
			int length=0;
			while (loc_idx - length >= 0 && loc_idx + length < str_size && s[loc_idx - length] == s[loc_idx + length]){
				length++;
			}
			if (2*length-1 >max_length){
				max_length = 2 * length - 1;
				location = loc_idx - length + 1;
			}

		}
		//even size
		for (int loc_idx = 0; loc_idx < str_size; loc_idx++){
			int length=0;
			while (loc_idx - length >= 0 && loc_idx + length + 1 < str_size && s[loc_idx - length] == s[loc_idx + length + 1]){
				length++;
			}
			if (2 * length > max_length){
				max_length = 2 * length;
				location = loc_idx-length+1;
			}
		}
		return s.substr(location, max_length);
	}
};

int main(){
	//string A; cin >> A;
	string A = "babad";
	string B = "cbbd";
	string C = "bb";
	Solution Solution;
	cout << A << endl;
	cout << Solution.longestPalindrome(A) << endl;
	cout << B << endl;
	cout << Solution.longestPalindrome(B) << endl;
	cout << C << endl;
	cout << Solution.longestPalindrome(C) << endl;
	int end; cin >> end;
	return 0;
}

实际操作的时候,一定要找好映射与边界,最好将实际的string画出来,相应的index标上。

  • 单映射,比如ababa的时候,需要将loc_idx表示为中间元素的位置
  • 满足条件的前面边界为loc_idx-length>=0, 后面边界<str_size
  • while循环退出的时候,length多加了1
  • 映射到字符串的前面边界为 loc_idx-length +1
  • 映射到子字符串的长度为 2*length-1
  • 偶数长度比如abba的字符串类推

7.4 插入#简化映射

先加#,加#之后的映射变得比之前更简单和易得

class Solution {
public:
	string longestPalindrome(string s) {
		int str_size = s.size();
		//add #
		string add_s = "#";
		for (int idx = 0; idx < str_size; idx++){
			add_s += s[idx];
			add_s += "#";
		}
		int add_length = 2 * str_size + 1;

		//找出加了#后的最长长度和位置
		int location = 0;
		int max_length = 0;
		for (int loc_idx = 0; loc_idx < add_length; loc_idx++){
			int length = 0;
			while (loc_idx - length >= 0 && loc_idx + length < add_length && add_s[loc_idx - length] == add_s[loc_idx + length]){
				length++;
			}
			if (length>max_length){
				max_length = length;
				location = loc_idx;
			}
		}
		// 找出映射
		int begin=location-max_length+1;
		
		return s.substr(begin/2, max_length-1);
	}
};

7.5 manacher法

原理参考:

https://www.cnblogs.com/mini-coconut/p/9074315.html

程序待补充

发布了210 篇原创文章 · 获赞 584 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/weixin_36474809/article/details/100044593