算法小題:最大連續子序列和問題

前言

刷題的時候遇到了這個最大連續子序列和的問題,查閱研究了一下。記錄在此。希望能夠便人便己。

問題描述

給定一個數組a[ n ] ,數組元素均爲自然數集(有正數,有負數),請求出該數組一個連續的子序列,使得這個子序列的和值最大,示例如下
a[] = {1, 2, -9, 5, 6, -3, 7, 8, -89, 10}
那麼它的最大連續子序列爲 {5,6,-3,7,8} ,和值 = 23

說明:偷懶了一下,問題描述從其他博客中cv一波。這裏要稍微注意一點,就是數組內的元素可以全是負數,這樣的話最大的子序列和有的是返回0,有的是返回最大的負數。

好,問題很明確,廢話不多說,直接上解法。

解法一

分析

最簡單的方式就是暴力解法。你不是讓求出最大的連續子序列和嗎?那我把所有的情況都遍歷一遍,不就行了。不動腦子,簡單粗暴,爽的一批。

那所有的情況又有哪些呢?

很容易想到,有可能是
a0,a0-a1,a0-a2,…,a0-a(n-1)
也有可能是
a1,a1-a2,a1-a3,…a1-a(n-1)

簡單類推一下,就可以得出一個O(N2)的解法。
不贅述多說,上代碼。

代碼

//暴力求解
int MaxSum(int A[],int n)
{
	int maximum = -INF;		//一開始賦值爲最小的
	for(int i = 0;i<n;++i)
	{
		sum+=A[j];
		if(sum>maximum)
			maximum = sum;
	}
	return maximum;
}

簡單吧,是不是不敢相信自己的眼睛。但是,代碼行數雖然不多,但是其確實循環了兩層,有着O(N2)的複雜度。

解法二

分析

好的,讓我們開動開動自己那聰明的大腦,想一想有沒有更好一些的方法。

嗯…
啊…
突然間,靈光一閃。 你想到了這裏面可能有重複的子問題的性質呢(好吧,你沒想到,我也沒想到,我也是看了答案纔想到的 ̄□ ̄||),能不能利用類似二分查找那樣的方式,進行二分搜索最大的子序列和呢?

在這裏插入圖片描述

像上面那樣,從中間截開分成兩段B和C,那麼我們可以得到下面這個結論:
最終的連續子序列
(1)、要麼在B中,
(2)、要麼在C中
(3)、要麼在包含中間分界線的兩個元素(這裏是6和-3)的一個序列中。

最終確定是哪個序列,那就開看這三種情況下哪個更大了。

(搞不定?聽不懂?別啊,相信你,仔細看,畫個小圖慢慢想,you will get it。)
然後我們,在B和C中按照相同思路繼續遞歸即可。

好了,理論上完了,來點實踐的。

代碼

//遞歸算法求解最大連續子序列和
int MaxSum1(int data[],int beg,int end)
{
    if(beg>=end)
    {
        return data[beg];
    }
    else
    {
        int mid = (end+beg)/2;
        int leftMax = MaxSum1(data,beg,mid);       //左邊最大序列
        int rightMax = MaxSum1(data,mid+1,end);    //右邊最大序列

        //包含中間兩個數字的最大序列
        int tmpLeftMax = INF;       //這裏INF = -10000
        int tmpSum = 0;
        for(int j = mid;j>=beg;--j)
        {
            tmpSum+=data[j];
            if(tmpSum>tmpLeftMax)
                tmpLeftMax = tmpSum;
        }

        int tmpRigMax = INF;
        tmpSum = 0;
        for(int j = mid+1; j<=end;++j)
        {
            tmpSum+=data[j]  ;
            if(tmpSum>tmpRigMax)
                    tmpRigMax = tmpSum;
        }

		//判斷三種情況哪個更大些
        int retSum = leftMax;
        if(retSum<rightMax)
            retSum = rightMax;

        if(retSum<tmpLeftMax+tmpRigMax)
            retSum = tmpLeftMax+tmpRigMax;
        return retSum;
    }
}

注:很多線性表有關的題都有重複子問題的性質,都可以利用類似的遞歸算法進行分解。本算法是其中的一種,達到的時間複雜度是O(N*LogN)

解法三

分析

如果你想到了要求解的問題具有重複的子問題,那麼你有沒有想到這個問題中具有重疊的子問題,以及最優子結構呢? 如果這你都想到了,那麼,大神,留個方式唄,我想和你做朋友。

其實這裏是可以用動態規劃來做的。
其遞推關係式就是

**sum[i]=max(sum[i-1]+data[i],data[i])**

其中,sum[i]表示序列結尾元素是第i個元素的最大序列和。比如說,
sum[5]代表序列結尾是a5的所有連續序列中和最大的那個值。有可能是
a0-a5,a1-a5,a2-a5,a3-a5,a4-a5,a5 。

別問我是怎麼想到這個遞推公式的,你懂的。 但是,我用了我的很多的腦細胞模模糊糊想出了一點東西,與君共勉。

對於大多數問題來說,我們的求解思路只要考慮到所有的解空間就可以(當然不一定需要每個都比較,否則就變成了暴力求解了)。

對於這題來說,答案肯定是a[i]-a[j] 的某個序列,不用想,j可以是0-n-1。
所以,我把j=0的序列最大值求出來,然後j=1的序列最大值求出來,…,然後在遍歷一次,最終就可以確定出整體的最優值。

從某種程度上來說,我的上述分解空間的想法有點像先確定一個條件,把符合條件所有的情況都算出來,然後在看有多少個這樣的條件,進而分解全部的空間。在實際中,這種思路也有不少應用。

好了,說了這麼多,下面該來點硬貨了。

代碼

//動態規劃求解: sum[i]=max(sum[i-1]+data[i],data[i])
int MaxSum(int data[],int n)
{
    int sum[n];
    sum[0] = data[0];

  //包含data[...,i]的和最大值
    for(int i = 1;i<n;++i)
    {
        if(sum[i-1] > 0)
            sum[i] = sum[i-1]+data[i];
        else
            sum[i] = data[i];
    }

    int max = sum[0];
    for(int i = 1;i<n;++i)
    {
        if(sum[i] > max)
            max = sum[i];
    }
    return max;
}

解法三(番外版)

分析

我的小夥伴,你的大腦累了嗎? 累了,好咱來玩個遊戲。

想象一下,你在一條放着金幣和陷阱的公路上行走。沒進過一個金幣站點,你就可以獲得對應的金幣,沒經過一個陷阱站點,你就必須損失相應的金幣數量。 你可以選擇從任意一個站點開始,也可選擇在任意一個站點結束。但是,在開始和結束的站點之間的這段連續站點內,必須一直走下去。

ps:遊戲你可以重玩多次,這樣的話,你就可以記錄下上一次的發現成果,如下圖所示,陷阱我已經給你圈出來了哦。

在這裏插入圖片描述
好吧,夥伴,你會怎麼玩?

反正我是這樣玩的:
我手裏拿着個小本本,在玩的過程中一直記錄兩個值,一個是到目前爲止獲得的最大金幣數量A,一個是到當前位置還剩餘的金幣數量B。注意這兩個值不一樣,前者是所以連續序列段的最大的金幣數量,後者是從一個序列的開始到目前的位置這段連續的站點所獲得的金幣數量,這是指的是當前連續序列段。

出發之前,我悟到了一個遊戲策略:選好每個連續序列的起點。

我是怎麼選的呢,我就放心大膽的往前走,直到我當前還剩餘的金幣數量B小於0了,那麼我就需要重新開啓一個新的連續序列了。(遇到陷阱不可怕,只要我當前的金幣數量夠支付陷阱的,那我就可能在後面遇到更大的金幣)當然,在整個過程中,我需要記錄最大的金幣數量A。
在這裏插入圖片描述
Ok。如果不出意外的話,我就可以在O(N)的時間內獲得最大的連續序列和。

好了,遊戲玩完了,下面改上實際的代碼了。

代碼

//番外動態規劃
int MaxSum2(int data[],int n)
{
    int allMax = INF;
    int seqSum = 0;
    for(int i = 0;i<n;++i)
    {
        if(data[i]+seqSum < 0)      //當前的值加上前面序列之和小於零
        {
            seqSum = 0;         //從下一個元素開始重新更換序列起始位置
        }
        else
        {
            seqSum+=data[i];
            if(seqSum > allMax)     //如果當前序列值的和大於allMax
                allMax = seqSum;

        }
    }
    if(allMax == INF)       //如果全部是負數的話,返回0,(或者重新遍歷,返回最大負數)
        allMax = 0;

    return allMax;
}

注:
(1)、當然:這裏有點小瑕疵,就是如果輸入全部是負數的話,返回的是0,而不是最小的負數。
(2)、本程序時間上的複雜度就是O(N)
(3)、本質上,這裏面用的還是動態規劃的思想。不過,只是出發點比較稍微形象店。

實驗結果

好了,最終的實驗 結果如下(分別是後三種答案,第一種就不測試了):
在這裏插入圖片描述

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