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)

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

程序待補充

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章