P-數學程序猿今天終於要拿起筆來寫寫畫畫了٩(◕‿◕。)۶,ios
問題來了——最大子列和算法
今天咱們就來談談最大子列和問題吧,數組
要從一連串的數字中找到最大的連續子序列的和,數據結構
就好比數組[-2,1,-3,4,-1,2,1,-5,4],連續子數組 [4,-1,2,1] 的和最大,爲 6,app
可連續子序列有這麼多,哪一個纔是最大的呀,還真是讓人頭疼啊 ༼ ╥ ل ╥ ༽dom
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ide
問題建模函數
話很少說,先爲咱們的問題構建測試代碼吧(算法纔是關鍵,此處可忽略,雖然這裏的代碼纔是最耗時間和耐心的)測試
1 #include <iostream> 2 #include <vector> 3 #include <ctime> 4 using namespace std; 5 6 #define random(x) -x+1+rand()%(2*x-1) //用宏來定義隨機數,生成(-x,x)的隨機整數 7 8 void fillRandom_Int(vector<int> & nums,int cnt,int maxmum);//生成隨機數組 9 int maxSubArray_test(vector<int>& nums);//引用leetcode上的官方題解,用於驗證 10 bool judge(vector<int> & nums,int ans); //利用leetcode上的官方題解進行答案驗證 11 int maxSubArray1(vector<int> & nums); //三重暴力循環 12 int maxSubArray2(vector<int> & nums); //兩重暴力循環 13 int maxSubArray3(vector<int> & nums); //分而治之 14 int maxSubArray4(vector<int> & nums); //在線處理(貪心算法) 15 int maxSubArray5(vector<int> & nums); //動態規劃 16 void test(int datasize,int (*function)(vector<int> &)); //測試並輸出測試結果 17 18 int main() 19 { 20 srand((int)time(NULL)); //產生隨機數種子 21 cout << endl; 22 /*cout << "\t三重暴力循環 結果 用時" << endl; 23 cout << "\t隨機測試點1";test(10,maxSubArray1); 24 cout << "\t隨機測試點2";test(100,maxSubArray1); 25 cout << "\t隨機測試點3";test(1000,maxSubArray1); 26 cout << "\t隨機測試點4 超出時間限制" << endl; 27 cout << "\t隨機測試點5 超出時間限制" << endl; 28 cout << endl << endl; 29 cout << "\t兩重暴力循環 結果 用時" << endl; 30 cout << "\t隨機測試點1";test(10,maxSubArray2); 31 cout << "\t隨機測試點2";test(100,maxSubArray2); 32 cout << "\t隨機測試點3";test(1000,maxSubArray2); 33 cout << "\t隨機測試點4";test(10000,maxSubArray2); 34 cout << "\t隨機測試點5 超出時間限制" << endl; 35 cout << endl << endl;*/ 36 cout << "\t分而治之 結果 用時" << endl; 37 cout << "\t隨機測試點1";test(10,maxSubArray3); 38 cout << "\t隨機測試點2";test(100,maxSubArray3); 39 cout << "\t隨機測試點3";test(1000,maxSubArray3); 40 cout << "\t隨機測試點4";test(10000,maxSubArray3); 41 cout << "\t隨機測試點5";test(100000,maxSubArray3); 42 cout << endl << endl; 43 cout << "\t貪心算法 結果 用時" << endl; 44 cout << "\t隨機測試點1";test(10,maxSubArray4); 45 cout << "\t隨機測試點2";test(100,maxSubArray4); 46 cout << "\t隨機測試點3";test(1000,maxSubArray4); 47 cout << "\t隨機測試點4";test(10000,maxSubArray4); 48 cout << "\t隨機測試點5";test(100000,maxSubArray4); 49 cout << endl << endl; 50 cout << "\t動態規劃 結果 用時" << endl; 51 cout << "\t隨機測試點1";test(10,maxSubArray5); 52 cout << "\t隨機測試點2";test(100,maxSubArray5); 53 cout << "\t隨機測試點3";test(1000,maxSubArray5); 54 cout << "\t隨機測試點4";test(10000,maxSubArray5); 55 cout << "\t隨機測試點5";test(100000,maxSubArray5); 56 return 0; 57 } 58 59 void fillRandom_Int(vector<int> & nums,int cnt,int maxmum) 60 { //nums爲生成的隨機數組,cnt爲數組元素個數,mammum爲數組元素範圍 61 nums.resize(cnt); 62 for(int i = 0;i < cnt;++i) 63 nums[i] = random(maxmum); 64 } 65 66 int maxSubArray_test(vector<int> & nums) 67 { 68 int n = nums.size(); 69 int currSum = nums[0],maxSum = nums[0]; 70 for(int i = 1;i < n;++i){ 71 currSum = max(nums[i],currSum+nums[i]); 72 maxSum = max(maxSum,currSum); 73 }return maxSum; 74 } 75 76 bool judge(vector<int> & nums,int ans) 77 { 78 return ans == maxSubArray_test(nums); 79 } 80 81 void test(int datasize,int (*function)(vector<int> &)) 82 { //輸入測試數據規模,測試並輸出測試結果 83 vector<int> nums; 84 fillRandom_Int(nums,datasize,100); //生成隨機數組 85 clock_t start = clock(); 86 int ans = function(nums); //調用測試函數 87 clock_t stop = clock(); 88 if(judge(nums,ans)){ 89 cout << " 答案正確 " 90 << (double(stop-start))/CLK_TCK << 's' << endl; 91 }else cout << " 答案錯誤" << endl; 92 }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~spa
解決辦法
下面就來看看最大子列和的五種解決辦法吧,
方法 | 時間複雜度 | 空間複雜度 |
三重循環 | O(N^3) | O(1) |
兩重循環 | O(N^2) | O(1) |
分而治之 | O(NlogN) | O(logN) |
貪心算法 | O(N) | O(1) |
動態規劃 | O(N) | O(N)或O(1) |
方法一——暴力三重循環
秉承着「能用暴力解決的問題就暴力解決」的原則,咱們先來看看暴力的方法,最原始,最簡單,最暴力的方法莫過於遍歷全部連續子序列,取它們各自的和的最大值
詳細以下
int maxSubArray1(vector<int> & nums) { //三重暴力循環 int n = nums.size();int sum;int ans = INT_MIN; for(int i = 0;i < n;++i){ //i對應子序列最左邊下標 for(int j = i;j < n;++j){ //j對應子序列最右邊下標 sum = 0; for(int k = i;k <= j;++k) //將子序列各項加起來 sum += nums[k]; if(sum > ans) ans = sum; } }return ans; }
其中i對應子序列最左邊下標,j對應子序列最右邊下標,用兩重循環遍歷全部子序列,
再用一重循環獲取子序列的和,取其最大值便可
測試結果以下(PS:各個測試點數據規模爲10,100,1000,10000,100000,時間限制爲10s,後面再也不贅述)
方法二——暴力兩重循環
俗話說「暴力也是講究美學的」,能不能把O(N^3)的複雜度降下來呢?
暴力法,前面的兩重循環遍歷全部子序列是避免不了了,然而,那循環計算子列和倒是值得琢磨琢磨
咱們留意到,在第二重循環裏,當前子列和正是前一個子列和再加上當前子列的最右邊元素,即nums[j],
因而,咱們能夠用一個臨時變量sum存儲子列和,就能夠減小第三重循環,
這就有了第二個算法
1 int maxSubArray2(vector<int> & nums) 2 { //兩重暴力循環 3 int n = nums.size();int sum;int ans = INT_MIN; 4 for(int i = 0;i < n;++i){ 5 sum = 0; 6 for(int j = i;j < n;++j){ 7 sum += nums[j]; 8 if(sum > ans) ans = sum; 9 } 10 }return ans; 11 }
咱們能夠看到,複雜度也隨之從O(N^3)降到O(N^2)
測試結果以下(經過了測試4)
方法三——分而治之
行裏話講:「一個優秀的程序猿,當遇到O(N^2)複雜度的問題的時候,每每會想能不能把它降到O(NlogN)複雜度」,而辦法每每正是分而治之
所謂「分」,就是將原問題劃分紅兩個或多個子問題,
所謂「治」,就是將子問題及其餘可能的解治理成原問題的解
對應到最大子列和,就是將原序列等分紅兩個子序列,
則原序列的全部子序列,
一部分包含在左邊的子序列中,一部分包含在右邊的子序列中,
還有一部分子序列則跨越了劃分線,可記爲中間子序列,
獲取三部分子序列各自的最大子列和,三者最大值即爲原問題的解
代碼以下
1 #define max3(x,y,z) x > y ? (x > z ? x : z) : (y > z ? y : z) //三個數取最大數 2 3 int DivideAndConquer(vector<int> & nums,int left,int right){ 4 if(left == right) return nums[left]; 5 6 int mid = (left+right)/2; //將原序列等分 7 int lsum = DivideAndConquer(nums,left,mid); //遞歸獲取左邊的最大子序列和 8 int rsum = DivideAndConquer(nums,mid+1,right);//遞歸獲取右邊的最大子序列和 9 //以劃分線爲起點往左往右掃描整個數組獲取中間序列的最大子列和 10 int lbsum = INT_MIN,rbsum = INT_MIN,tmp = 0; 11 for(int i = mid;i >= left;--i){ //往左掃描 12 tmp += nums[i]; 13 if(tmp > lbsum) lbsum = tmp; 14 }tmp = 0; 15 for(int i = mid+1;i <= right;++i){//往右掃描 16 tmp += nums[i]; 17 if(tmp > rbsum) rbsum = tmp; 18 }return max3(lsum,rsum,lbsum+rbsum);//返回三者的最大值 19 } 20 int maxSubArray3(vector<int> & nums) 21 { //分而治之 22 int n = nums.size(); 23 return DivideAndConquer(nums,0,n-1); 24 }
測試結果以下(五個測試均沒超時)
方法四——貪心算法
咱們能夠經過子序列最右邊元素將全部子序列分爲N類,
從左往右掃描,每掃描一個元素,就至關於多一類子序列,
那麼這類子序列的最大子列和是多少呢?
若是前一類子序列的最大子列和非負,則是前一類子序列最大子列和加上掃描的元素;
否則則是掃描的元素自己;
如此獲取各種子序列的最大子列和的最大值即爲全部子序列的最大值
代碼以下
1 int maxSubArray4(vector<int> & nums) 2 { //貪心算法 3 int n = nums.size(); 4 int currSum = nums[0],maxSum = nums[0]; 5 for(int i = 1;i < n;++i){ 6 currSum = currSum < 0 ? nums[i] : currSum+nums[i]; 7 if(currSum > maxSum) maxSum = currSum; 8 }return maxSum; 9 }
測試結果以下(基本瞬出結果)(~ ̄▽ ̄)~
方法五——動態規劃
其實動態規劃換湯不換藥,跟貪心算法相似,
經過子序列最右邊元素將全部子序列分爲N類,
從左往右掃描,每掃描一個元素,就至關於多一類子序列,
用數組記錄各種子序列和,其中最大值即爲全部子序列的最大值
若新建數組,則空間複雜度爲O(N),若在原數組修改,則空間複雜度爲O(1)
代碼以下
1 int maxSubArray5(vector<int> & nums) 2 { //動態規劃 3 int n = nums.size(); 4 vector<int> sums = nums; 5 int maxSum = nums[0]; 6 for(int i = 1;i < n;++i){ 7 if(sums[i-1] >= 0) sums[i] += sums[i-1]; 8 if(sums[i] > maxSum) maxSum = sums[i]; 9 }return maxSum; 10 }
測試結果(一樣瞬出)(~ ̄▽ ̄)~
回顧5種算法,暴力,暴力美學,分而治之,貪心算法,動態規劃,將問題的複雜度從O(N^3)一直降到O(N),這就是算法的威力,也是我爲何選擇它做爲個人第一篇正式的博客,但願能對你們有所幫助和啓迪
此次寫博也是有很多缺陷的,好比動態規劃和貪心算法方面沒有畫圖詳細說明(但願之後能找到一個好的畫圖軟件,電腦手寫數字實在太醜了(ó﹏ò。))
最後,安利浙江大學陳越老師的慕課《數據結構》(★>U<★),本文不少都是從中得到啓發,
晚安(V●ᴥ●V),各位~~~~