陣列累加和問題三連

十一月的囂張發表於2020-11-09

陣列累加和問題三連

第一題

題目:

給定一個全是正數的陣列arr,一個目標數字target,求陣列中滿足和為target的最長子陣列的長度

思路:

這是很簡單的題目,用雙指標和視窗就可以解決。具體見程式碼

程式碼:

	public static int getMaxLength(int[] arr, int K) {
		if (arr == null || arr.length == 0 || K <= 0) {
			return 0;
		}
		int left = 0;
		int right = 0;
		int sum = arr[0];
		int len = 0;
		while (right < arr.length) {
			if (sum == K) {
				len = Math.max(len, right - left + 1);
				sum -= arr[left++];
			} else if (sum < K) {
				right++;
				if (right == arr.length) {
					break;
				}
				sum += arr[right];
			} else {
				sum -= arr[left++];
			}
		}
		return len;
	}

第二題

題目:

給定一個陣列arr,陣列中整數,負數和0都有,一個目標數字target,求陣列中滿足和為target的最長子陣列的長度

思路

對於這種陣列問題,要求某個S問題的解,一般來說有兩種解決思路

  1. 以陣列元素i作為起始位置時,S問題的解。遍歷所有的解,能得到最終答案
  2. 以陣列元素i作為終止位置時,S問題的解。遍歷所有的解,能得到最終答案

對於這個題,採用的第二種思路。
對於陣列元素a[j]來說,要求其作為子陣列最後一個元素時,滿足條件的子陣列的最大長度,對於這個問題來說,那麼需要找到一個a[i],使得sum(a[i],…,a[j])=target,同時,這個i要儘可能的小,這樣才能滿足子陣列最長。對於每一個a[j],都這樣做,最後遍歷結果,就可以找到最終的結果。

按照上面的思路,感覺時間複雜度是O(N^2)的。這裡可以繼續優化,我們可以在O(n)的時間內求出陣列的字首和S,如果想要得到一個最小的i,使得sum(a[i],…,a[j])=target,那麼就相當於找到一個i,使得S[i] = S[j] - target。當然,如果找不到滿足條件的i,說明以a[j]結尾的子陣列沒有滿足條件的,直接遍歷下一個就可以了。

時間複雜度為O(N),空間複雜度也是O(N)

程式碼

    public int maxSubArrayLen(int[] nums, int k) {
        // Write your code here
		if (nums == null || nums.length == 0) {
			return 0;
		}
		//用Map儲存出現過的字首和及其對應的索引
		HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
		map.put(0, -1); // 需要提前壓入一個鍵值對<0,-1>
		//如果不壓入的話,當從0到j正好滿足條件時,Map中找不到字首為0對應的索引,
		//所以會導致答案不對
		int len = 0;
		int sum = 0;
		for (int i = 0; i < nums.length; i++) {
			sum += nums[i];
			if (map.containsKey(sum - k)) {
				len = Math.max(i - map.get(sum - k), len);
			}
			if (!map.containsKey(sum)) { //對於同樣的字首和,只儲存第一次出現的位置,因為要保		    證子陣列最長
				map.put(sum, i);
			}
		}
		return len;

    }

變體

題目:給定一個陣列arr,稱1和2的個數相同的子陣列為合格的子陣列,求arr中合格的子陣列的最大長度

思路:
首先對陣列進行一次遍歷,對於每個元素,如果為1,則不做處理;如果為2,則變為-1;如果為其他數,則設定為0。那麼問題就轉化為,求arr中,滿足目標和為0的子陣列的最大長度。

第三題

題目

給定一個陣列arr,陣列中整數,負數和0都有,一個目標數字target,求陣列中滿足和小於等於target的最長子陣列的長度

思路

首先定義概念,定義兩個陣列,分別是

  • MinSum[]:MinSum[i]代表著子陣列以i位置元素開頭,能取得的最小累加和
  • MinSumEnd[]:MinSumEnd[i]代表著子陣列以i位置元素開頭,能取得最小累加和的子陣列的結束位置

MinSum[i]表示從i開始往後,所有子陣列的最小的累加和,MinSumEnd[i]代表著對應的結束位置,這兩個資料是非常有用的。

下面開始主流程:從陣列的index位置開始,設index = 0 ,利用上面兩個陣列,求出0位置作為子陣列開頭所能達到的滿足條件的子陣列的最大長度。很簡單,如果MinSum[0]<=target,那麼可以得到末尾索引j(j=MinSumEnd[i]),此時sum = MinSum[0],接下來計算sum +MinSum[j+1],如果sum<=target,則sum = sum +MinSum[j+1],並且得到新的末尾位置k(k=MinSumEnd[j+1]);繼續執行上面的操作直到 sum +MinSum[k+1] >target 時,停止,此時記錄0位置作為子陣列的最大長度(滿足條件的子陣列),記此時的子陣列末尾位置為p;
然後,sum = sum - arr[0],也就是將0位置元素從sum中減去,此時sum為從1到p位置所有元素的和,如果sum + MinSum[p+1] <=target,則sum = sum + MinSum[p+1],p = MinSumEnd[p+1],繼續迴圈;如果sum + MinSum[p+1] > target,說明以1為開頭的,能達到的滿足條件的子陣列,最大長度已經找到了,嘗試更新這個全域性的最大長度,然後將index++,重複上述過程。

這個演算法的精髓在於,在找到0位置開頭的滿足條件的最長子陣列之後,將0位置刪除以後,以1位置開始找滿足長度的子陣列時,不需要從頭開始找。假設從0開始的滿足條件的最大子陣列的長度是p+1,也就是到索引p位置終止(這就是說0到p的sum值,加上MinSum[p+1]的值,大於target),那麼當1為子陣列開頭元素時,如果1到p的sum值,加上MinSum[p+1]的值還是大於target的,說明1作為開始位置的,能達到滿足條件的子陣列中,長度不會超過p,這是比從0開始的滿足條件的最大子陣列的長度要小的,所以1開頭的子陣列就沒有繼續討論的意義了。

這個演算法的關鍵在於,除了第一次遍歷,後面的遍歷,都不需要從頭開始找,刪除了對那些不可能成為答案的結果的遍歷,所以可以達到O(N)

程式碼

	public static int maxLengthAwesome(int[] arr, int k) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		int[] minSums = new int[arr.length];
		int[] minSumEnds = new int[arr.length];
		//初始化兩個關鍵陣列
		minSums[arr.length - 1] = arr[arr.length - 1];
		minSumEnds[arr.length - 1] = arr.length - 1;
		for (int i = arr.length - 2; i >= 0; i--) {
			if (minSums[i + 1] < 0) {
				minSums[i] = arr[i] + minSums[i + 1];
				minSumEnds[i] = minSumEnds[i + 1];
			} else {
				minSums[i] = arr[i];
				minSumEnds[i] = i;
			}
		}
		
		int end = 0;
		int sum = 0;
		int res = 0;
		// i是視窗的最左的位置,end擴出來的最右有效塊兒的最後一個位置的,再下一個位置
		// end也是下一塊兒的開始位置
		// 視窗:[i~end)
		for (int i = 0; i < arr.length; i++) {
			// while迴圈結束之後:
			// 1) 如果以i開頭的情況下,累加和<=k的最長子陣列是arr[i..end-1],看看這個子陣列長度能不能更新res;
			// 2) 如果以i開頭的情況下,累加和<=k的最長子陣列比arr[i..end-1]短,更新還是不更新res都不會影響最終結果;
			while (end < arr.length && sum + minSums[end] <= k) {
				sum += minSums[end];
				end = minSumEnds[end]+1;
			}
			res = Math.max(res, end - i);
			if (end > i) { // 視窗內還有數 [i~end) [4,4)
				sum -= arr[i];
			} else { // 這是需要特殊考慮的一點,視窗內已經沒有數了,
			//說明從i開頭的所有子陣列累加和都不可能<=k,這時end=i了,讓end=  i+1,下一輪開始時
			//i也會加1,就自動跳到下一個數去遍歷了
				end = i + 1;
			}
		}
		return res;
	}

程式碼的迴圈中,兩個遍歷,i和end,都是從0跑到n,所以時間複雜度是O(N).

對於程式碼中end=i的情況,可以看下面這個例子
[-7,3,3,3,3,3,…],target=0
從0位置開始,可以最多到3位置,此時end = 3,i=0,sum = sum - (-7),i++
當i=1時,sum = 6 > target ,sum = sum-3,i++,
i=2時,sum = 3 >target,sum = sum-3,i++ ,
i=3時,sum = 0 =target(此時沒有元素在視窗中了,但是end仍然擴不動,此時的end==i,所以讓end = i+1,end變成4,下一輪i++也會變成4,就會跳過3位置的元素,繼續遍歷了)

相關文章