動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。
c++動態規劃類算法編程彙總(二)全排列| O(n)排序 | manacher法
c++策略類O(n)編程問題彙總(撲克的順子|約瑟夫環|整數1出現的次數|股票最大利潤)
目錄
一、加油站與油O(n)
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]}。
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,cab和cba。
思路要清晰,
- 每個節點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;
}
- 注意一點,set頭文件可以不重複的輸入進去,當作集合,用insert函數。對於aa,輸出只有aa,而不是[aa,aa],所以必須用set
- 編程序的時候,涉及到下標的,要畫出來具體化。
- 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
程序待補充