簡體   English   中英

使用回溯的所有子集的時間復雜度

[英]Time complexity for all subsets using backtracking

我試圖在使用回溯時了解時間復雜度。 問題是

給定一組唯一整數,返回所有可能的子集。 例如。 輸入 [1,2,3] 將返回 [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3 ]] 我正在使用回溯解決它,如下所示:

private List<List<Integer>> result = new ArrayList<>();

public List<List<Integer>> getSubsets(int[] nums) {
    
    for (int length = 1; length <= nums.length; length++) { //O(n)
        backtrack(nums, 0, new ArrayList<>(), length);
    }
    result.add(new ArrayList<>());
    return result;
}

private void backtrack(int[] nums, int index, List<Integer> listSoFar, int length) {
    if (length == 0) {
        result.add(listSoFar);
        return;
    }
    
    for (int i = index; i < nums.length; i++) { // O(n)
        List<Integer> temp = new ArrayList<>(); 
        temp.addAll(listSoFar);                 // O(2^n)
        temp.add(nums[i]);
        backtrack(nums, i + 1, temp, length - 1);
    }
}

代碼運行良好,但我無法理解時間/空間復雜度。

我在想的是這里遞歸方法被稱為 n 次。 在每次調用中,它生成可能包含最多 2^n 個元素的子列表。 所以時間和空間都是 O(nx 2^n),對嗎?

是對的嗎? 如果沒有,誰能詳細說明一下?

請注意,我在這里看到了一些答案,就像這樣但無法理解。 當遞歸出現在畫面中時,我發現我很難理解它。

您的代碼工作效率不高。

就像鏈接中的第一個解決方案一樣,您只考慮是否包含數量。 (比如獲得組合)

這意味着,您不必在 getSubsets 和回溯函數中進行迭代。 “backtrack”函數可以用參數迭代“nums”數組

private List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> getSubsets(int[] nums) {
    
    backtrack(nums, 0, new ArrayList<>(), new ArrayList<>());
    return result;
}

private void backtrack(int[] nums, int index, List<Integer> listSoFar) 
// This function time complexity 2^N, because will search all cases when the number included or not
{
    if (index == nums.length) {
        result.add(listSoFar);
        return;
    }
    
    // exclude num[index] in the subset 
    backtrack(nums, index+1, listSoFar)
    // include num[index] in the subset
    backtrack(nums, index+1, listSoFar.add(nums[index]))
}

您對空間復雜度的看法完全正確。 最終輸出的總空間為 O(n*2^n),這在程序使用的總空間中占主導地位。 不過,時間復雜度的分析略有偏差。 最佳情況下,在這種情況下,時間復雜度將與空間復雜度相同,但是這里存在一些低效率(其中之一是您實際上沒有回溯),因此時間復雜度實際上是 O(n^2 *2^n) 充其量。

根據遞歸方法被調用的次數乘以每次調用的工作量來分析遞歸算法的時間復雜度絕對有用。 但是要小心說backtrack只調用了n次:它在頂層被調用了n次,但這忽略了所有后續的遞歸調用。 也是在頂層的每次調用, backtrack(nums, 0, new ArrayList<>(), length); 負責生成所有lengthlength子集,其中有n Choose length 也就是說,沒有一個頂級調用會產生 2^n 個子集; 相反,從 0 到 n 的長度的n Choose length的總和是 2^n:

在此處輸入圖片說明

知道在所有遞歸調用中,您生成 2^n 個子集,然后您可能想知道在生成每個子集時完成了多少工作,以確定整體復雜性。 最佳情況下,這將是 O(n),因為每個子集的長度從 0 到 n 不等,平均長度為 n/2,所以整個算法可能是 O(n/2*2^n) = O(n *2^n),但您不能僅僅假設子集是最優生成的,並且沒有完成任何重要的額外工作。

在您的情況下,您正在通過listSoFar變量構建子集,直到它達到適當的長度,然后將其附加到結果中。 然而, listSoFar在 O(n) 時間內為每個 O(n) 字符復制到臨時列表,因此生成每個子集的復雜度為 O(n^2),這使整體復雜度為 O(n^ 2*2^n)。 此外,創建了一些listSoFar子集,它們永遠不會出現在最終輸出中(在listSoFar之前,您永遠不會檢查以查看nums是否有足夠的數字來將listSoFar填充到所需的length ),因此您最終會在構建子集時做不必要的工作並進行永遠不會達到基本情況的遞歸調用以附加到result ,這也可能會惡化漸近復雜性。 您可以通過回溯解決這些低效率中的第一個,然后使用簡單的 break 語句解決第二個問題。 我將這些更改寫入了 JavaScript 程序,大部分邏輯保持不變,但稍微重新命名/重新組織:

 function getSubsets(nums) { let subsets = []; for (let length = 0; length <= nums.length; length++) { // refactored "backtrack" function: genSubsetsByLength(length); // O(length*(n Choose length)) } return subsets; function genSubsetsByLength(length, i=0, partialSubset=[]) { if (length === 0) { subsets.push(partialSubset.slice()); // O(length): copy partial and push to result return; } while (i < nums.length) { if (nums.length - i < length) break; // don't build partial results that can't finish partialSubset.push(nums[i]); // O(1) genSubsetsByLength(length - 1, ++i, partialSubset); partialSubset.pop(); // O(1): this is the back-tracking part } } } for (let subset of getSubsets([1, 2, 3])) console.log(`[`, ...subset, ']');

關鍵的區別是使用回溯來避免每次添加新元素時都復制部分子集,這樣每個元素都是在 O(length) = O(n) 時間而不是 O(n^2) 中構建的時間,因為現在每個添加的元素只完成 O(1) 工作。 在每次遞歸調用后彈出添加到部分結果的最后一個字符允許您在遞歸調用中重復使用相同的數組,從而避免為每次調用制作temp副本的 O(n) 開銷。 這與僅構建出現在最終輸出中的子集這一事實一起,允許您根據輸出中所有子集的元素總數來分析總時間復雜度:O(n*2^n)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM