2015-05-02 34 views
4

請注意,這是必需的C#.NET 2.0項目(LINQ的不允許)。找到所有的k大小的子集有重複無序正整數的正尺寸袋和S

我知道非常類似的問題已經被問這裏,我已經產生了一些工作代碼(見下文),但仍想的意見,如何使算法更快給出K和S的條件。

這是我到目前爲止已經瞭解到: 動態編程是最有效的方式來找到一個(不是全部)子集。如果我錯了,請糾正我。是否有一種方法可以重複調用DP代碼來生成較新的子集,直到包(用重複項設置)耗盡?

如果沒有,那麼有沒有一種方法可以加快我下面的回溯遞歸算法,它會產生我需要的但運行在O(2^n),我認爲考慮s和k?

這是我的永遠不會在n = 114和數量範圍爲3〜286改變數字的固定袋:

int[] numbers = new int[] 
    { 
     7, 286, 200, 176, 120, 165, 206, 75, 129, 109, 
     123, 111, 43, 52, 99, 128, 111, 110, 98, 135, 
     112, 78, 118, 64, 77, 227, 93, 88, 69, 60, 
     34, 30, 73, 54, 45, 83, 182, 88, 75, 85, 
     54, 53, 89, 59, 37, 35, 38, 29, 18, 45, 
     60, 49, 62, 55, 78, 96, 29, 22, 24, 13, 
     14, 11, 11, 18, 12, 12, 30, 52, 52, 44, 
     28, 28, 20, 56, 40, 31, 50, 40, 46, 42, 
     29, 19, 36, 25, 22, 17, 19, 26, 30, 20, 
     15, 21, 11, 8, 8, 19, 5, 8, 8, 11, 
     11, 8, 3, 9, 5, 4, 7, 3, 6, 3, 
     5, 4, 5, 6 
    }; 

要求

  • 空間限制爲2-3GB最大但時間應該是O(N ^東​​西)不是 (東西^ n)。

  • 袋不得進行排序和複製不得拆除。

  • 結果應該是匹配 子集中的數字的索引,而不是數字本身(因爲我們有重複)。

動態規劃嘗試

這是改編自一個答案C#動態規劃版本,以一個類似的問題在這裏stackoverflow.com:

using System; 
using System.Collections.Generic; 

namespace Utilities 
{ 
    public static class Combinations 
    { 
     private static Dictionary<int, bool> m_memo = new Dictionary<int, bool>(); 
     private static Dictionary<int, KeyValuePair<int, int>> m_previous = new Dictionary<int, KeyValuePair<int, int>>(); 
     static Combinations() 
     { 
      m_memo.Clear(); 
      m_previous.Clear(); 
      m_memo[0] = true; 
      m_previous[0] = new KeyValuePair<int, int>(-1, 0); 

     } 

     public static bool FindSubset(IList<int> set, int sum) 
     { 
      //m_memo.Clear(); 
      //m_previous.Clear(); 
      //m_memo[0] = true; 
      //m_previous[0] = new KeyValuePair<int, int>(-1, 0); 

      for (int i = 0; i < set.Count; ++i) 
      { 
       int num = set[i]; 
       for (int s = sum; s >= num; --s) 
       { 
        if (m_memo.ContainsKey(s - num) && m_memo[s - num] == true) 
        { 
         m_memo[s] = true; 

         if (!m_previous.ContainsKey(s)) 
         { 
          m_previous[s] = new KeyValuePair<int, int>(i, num); 
         } 
        } 
       } 
      } 

      return m_memo.ContainsKey(sum) && m_memo[sum]; 
     } 
     public static IEnumerable<int> GetLastIndex(int sum) 
     { 
      while (m_previous[sum].Key != -1) 
      { 
       yield return m_previous[sum].Key; 
       sum -= m_previous[sum].Value; 
      } 
     } 

     public static void SubsetSumMain(string[] args) 
     { 
      int[] numbers = new int[] 
     { 
      7, 286, 200, 176, 120, 165, 206, 75, 129, 109, 
      123, 111, 43, 52, 99, 128, 111, 110, 98, 135, 
      112, 78, 118, 64, 77, 227, 93, 88, 69, 60, 
      34, 30, 73, 54, 45, 83, 182, 88, 75, 85, 
      54, 53, 89, 59, 37, 35, 38, 29, 18, 45, 
      60, 49, 62, 55, 78, 96, 29, 22, 24, 13, 
      14, 11, 11, 18, 12, 12, 30, 52, 52, 44, 
      28, 28, 20, 56, 40, 31, 50, 40, 46, 42, 
      29, 19, 36, 25, 22, 17, 19, 26, 30, 20, 
      15, 21, 11, 8, 8, 19, 5, 8, 8, 11, 
      11, 8, 3, 9, 5, 4, 7, 3, 6, 3, 
      5, 4, 5, 6 
     }; 

      int sum = 400; 
      //int size = 4; // don't know to use in dynamic programming 

      // call dynamic programming 
      if (Numbers.FindSubset(numbers, sum)) 
      { 
       foreach (int index in Numbers.GetLastIndex(sum)) 
       { 
        Console.Write((index + 1) + "." + numbers[index] + "\t"); 
       } 
       Console.WriteLine(); 
      } 
      Console.WriteLine(); 

      Console.ReadKey(); 
     } 
    } 
} 

遞歸程序嘗試

這裏是C#遞歸編程ve rsion改編自回答類似的問題在這裏stackoverflow.com:

using System; 
using System.Collections.Generic; 

namespace Utilities 
{ 
    public static class Combinations 
    { 
     private static int s_count = 0; 
     public static int CountSubsets(int[] numbers, int index, int current, int sum, int size, List<int> result) 
     { 
      if ((numbers.Length <= index) || (current > sum)) return 0; 
      if (result == null) result = new List<int>(); 

      List<int> temp = new List<int>(result); 
      if (current + numbers[index] == sum) 
      { 
       temp.Add(index); 
       if ((size == 0) || (temp.Count == size)) 
       { 
        s_count++; 
       } 
      } 
      else if (current + numbers[index] < sum) 
      { 
       temp.Add(index); 
       CountSubsets(numbers, index + 1, current + numbers[index], sum, size, temp); 
      } 

      CountSubsets(numbers, index + 1, current, sum, size, result); 
      return s_count; 
     } 

     private static List<List<int>> m_subsets = new List<List<int>>(); 
     public static List<List<int>> FindSubsets(int[] numbers, int index, int current, int sum, int size, List<int> result) 
     { 
      if ((numbers.Length <= index) || (current > sum)) return m_subsets; 
      if (result == null) result = new List<int>(); 

      List<int> temp = new List<int>(result); 
      if (current + numbers[index] == sum) 
      { 
       temp.Add(index); 
       if ((size == 0) || (temp.Count == size)) 
       { 
        m_subsets.Add(temp); 
       } 
      } 
      else if (current + numbers[index] < sum) 
      { 
       temp.Add(index); 
       FindSubsets(numbers, index + 1, current + numbers[index], sum, size, temp); 
      } 

      FindSubsets(numbers, index + 1, current, sum, size, result); 

      return m_subsets; 
     } 

     public static void SubsetSumMain(string[] args) 
     { 
      int[] numbers = new int[] 
     { 
      7, 286, 200, 176, 120, 165, 206, 75, 129, 109, 
      123, 111, 43, 52, 99, 128, 111, 110, 98, 135, 
      112, 78, 118, 64, 77, 227, 93, 88, 69, 60, 
      34, 30, 73, 54, 45, 83, 182, 88, 75, 85, 
      54, 53, 89, 59, 37, 35, 38, 29, 18, 45, 
      60, 49, 62, 55, 78, 96, 29, 22, 24, 13, 
      14, 11, 11, 18, 12, 12, 30, 52, 52, 44, 
      28, 28, 20, 56, 40, 31, 50, 40, 46, 42, 
      29, 19, 36, 25, 22, 17, 19, 26, 30, 20, 
      15, 21, 11, 8, 8, 19, 5, 8, 8, 11, 
      11, 8, 3, 9, 5, 4, 7, 3, 6, 3, 
      5, 4, 5, 6 
     }; 

      int sum = 17; 
      int size = 2; 

      // call backtracking recursive programming 
      Console.WriteLine("CountSubsets"); 
      int count = Numbers.CountSubsets(numbers, 0, 0, sum, size, null); 
      Console.WriteLine("Count = " + count); 
      Console.WriteLine(); 

      // call backtracking recursive programming 
      Console.WriteLine("FindSubsets"); 
      List<List<int>> subsets = Numbers.FindSubsets(numbers, 0, 0, sum, size, null); 
      for (int i = 0; i < subsets.Count; i++) 
      { 
       if (subsets[i] != null) 
       { 
        Console.Write((i + 1).ToString() + ":\t"); 
        for (int j = 0; j < subsets[i].Count; j++) 
        { 
         int index = subsets[i][j]; 
         Console.Write((index + 1) + "." + numbers[index] + " "); 
        } 
        Console.WriteLine(); 
       } 
      } 
      Console.WriteLine("Count = " + subsets.Count); 

      Console.ReadKey(); 
     } 
    } 
} 

請讓我知道如何限制的動態規劃版本大小爲k的子集,如果我能反覆調用它,所以它返回不同的子集直到沒有更多的匹配子集。

而且我不知道從哪裏初始化DP算法的備忘錄。我在靜態構造函數中做了這個,它在訪問任何方法時自動運行。這是正確的初始化位置,還是需要將它移動到FindSunset()方法[註釋掉]內部?

對於遞歸版本,它是回溯?我們如何加快速度。它工作正常,並考慮到k和s,但完全沒有效率。

讓我們讓這個線程成爲所有C#SubsetSum相關問題的母親!

+0

請不要只是在我們身上丟棄一層代碼,並要求我們爲您查看它。我們有[codereview.se]爲此目的。 –

+1

如果且僅當代碼按預期工作時,問題纔可能成爲代碼複審的主題。這個短語:_「請讓我知道如何將動態編程版本限制爲大小爲k的子集」,這聽起來像所期望的行爲尚未寫入。 – Phrancis

+0

有兩個版本: –

回答

2

我以前的答案對工作的原則切斷要檢查的組合數量。但是,一旦對數組排序,這可以得到顯着改善。原理是相似的,但由於解決方案完全不同,我決定把它放在一個單獨的答案。

我小心只使用.Net Framework 2.0功能。稍後可能會添加視覺解釋,但評論應該足夠了。

class Puzzle 
{ 
    private readonly int[] _tailSums; 
    public readonly SubsetElement[] Elements; 
    public readonly int N; 

    public Puzzle(int[] numbers) 
    { 
     // Set N and make Elements array 
     // (to remember the original index of each element) 
     this.N = numbers.Length; 
     this.Elements = new SubsetElement[this.N]; 

     for (var i = 0; i < this.N; i++) 
     { 
      this.Elements[i] = new SubsetElement(numbers[i], i); 
     } 

     // Sort Elements descendingly by their Number value 
     Array.Sort(this.Elements, (a, b) => b.Number.CompareTo(a.Number)); 

     // Save tail-sums to allow immediate access by index 
     // Allow immedate calculation by index = N, to sum 0 
     this._tailSums = new int[this.N + 1]; 
     var sum = 0; 

     for (var i = this.N - 1; i >= 0; i--) 
     { 
      this._tailSums[i] = sum += this.Elements[i].Number; 
     } 
    } 

    public void Solve(int s, Action<SubsetElement[]> callback) 
    { 
     for (var k = 1; k <= this.N; k++) 
      this.Solve(k, s, callback); 
    } 

    public void Solve(int k, int s, Action<SubsetElement[]> callback) 
    { 
     this.ScanSubsets(0, k, s, new List<SubsetElement>(), callback); 
    } 

    private void ScanSubsets(int startIndex, int k, int s, 
          List<SubsetElement> subset, Action<SubsetElement[]> cb) 
    { 
     // No more numbers to add, and current subset is guranteed to be valid 
     if (k == 0) 
     { 
      // Callback with current subset 
      cb(subset.ToArray()); 
      return; 
     } 

     // Sum the smallest k elements 
     var minSubsetStartIndex = this.N - k; 
     var minSum = this._tailSums[minSubssetStartIndex]; 

     // Smallest possible sum is greater than wanted sum, 
     // so a valid subset cannot be found 
     if (minSum > s) 
     { 
      return; 
     } 

     // Find largest number that satisfies the condition 
     // that a valid subset can be found 
     minSum -= this.Elements[minSubsetStartIndex].Number; 

     // But remember the last index that satisfies the condition 
     var minSubsetEndIndex = minSubsetStartIndex; 

     while (minSubsetStartIndex > startIndex && 
       minSum + this.Elements[minSubsetStartIndex - 1].Number <= s) 
     { 
      minSubsetStartIndex--; 
     } 

     // Find the first number in the sorted sequence that is 
     // the largest number we just found (in case of duplicates) 
     while (minSubsetStartIndex > startIndex && 
       Elements[minSubsetStartIndex] == Elements[minSubsetStartIndex - 1]) 
     { 
      minSubsetStartIndex--; 
     } 

     // [minSubsetStartIndex .. maxSubsetEndIndex] is the 
     // full range we must check in recursion 

     for (var subsetStartIndex = minSubsetStartIndex; 
      subsetStartIndex <= minSubsetEndIndex; 
      subsetStartIndex++) 
     { 
      // Find the largest possible sum, which is the sum of the 
      // k first elements, starting at current subsetStartIndex 
      var maxSum = this._tailSums[subsetStartIndex] - 
         this._tailSums[subsetStartIndex + k]; 

      // The largest possible sum is less than the wanted sum, 
      // so a valid subset cannot be found 
      if (maxSum < s) 
      { 
       return; 
      } 

      // Add current number to the subset 
      var x = this.Elements[subsetStartIndex]; 
      subset.Add(x); 

      // Recurse through the sub-problem to the right 
      this.ScanSubsets(subsetStartIndex + 1, k - 1, s - x.Number, subset, cb); 

      // Remove current number and continue loop 
      subset.RemoveAt(subset.Count - 1); 
     } 
    } 

    public sealed class SubsetElement 
    { 
     public readonly int Number; 
     public readonly int Index; 

     public SubsetElement(int number, int index) 
     { 
      this.Number = number; 
      this.Index = index; 
     } 

     public override string ToString() 
     { 
      return string.Format("{0}({1})", this.Number, this.Index); 
     } 
    } 
} 

用途和性能測試:

private static void Main() 
{ 
    var sw = Stopwatch.StartNew(); 
    var puzzle = new Puzzle(new[] 
     { 
      7, 286, 200, 176, 120, 165, 206, 75, 129, 109, 
      123, 111, 43, 52, 99, 128, 111, 110, 98, 135, 
      112, 78, 118, 64, 77, 227, 93, 88, 69, 60, 
      34, 30, 73, 54, 45, 83, 182, 88, 75, 85, 
      54, 53, 89, 59, 37, 35, 38, 29, 18, 45, 
      60, 49, 62, 55, 78, 96, 29, 22, 24, 13, 
      14, 11, 11, 18, 12, 12, 30, 52, 52, 44, 
      28, 28, 20, 56, 40, 31, 50, 40, 46, 42, 
      29, 19, 36, 25, 22, 17, 19, 26, 30, 20, 
      15, 21, 11, 8, 8, 19, 5, 8, 8, 11, 
      11, 8, 3, 9, 5, 4, 7, 3, 6, 3, 
      5, 4, 5, 6 
     }); 

    puzzle.Solve(2, 17, PuzzleOnSubsetFound); 

    sw.Stop(); 
    Console.WriteLine("Subsets found: " + _subsetsCount); 
    Console.WriteLine(sw.Elapsed); 
} 

private static int _subsetsCount; 

private static void PuzzleOnSubsetFound(Puzzle.SubsetElement[] subset) 
{ 
    _subsetsCount++; 
    return; // Skip prints when speed-testing 

    foreach (var el in subset) 
    { 
     Console.Write(el.ToString()); 
     Console.Write(" "); 
    } 

    Console.WriteLine(); 
} 

輸出:

每一行是一個發現子集,在數字爲數量在所使用的原始索引子集

14(60)3(107)
14(60)3(109)
14(60)3(102)
13(59)4(105)
13(59)4(111)
12(64)5(96)
12(64)5(104)
12(64)5(112)
12(64)5(110)
12(65)5(96)
12(65)5(104)
12(65)5(112)
12(65)5(110)
11(100)6(108)
11(100)6(113)
11(61)6(108)
11(61)6(113)
11(92)6(108)
11(92)6(113)
11(62)6(108)
11(62)6(113)
11(99)6(108)
11(99)6(113)
9(103)8(93)
(103)8(94)
9(103)8(97)
9(103)8(98)
9(103)8(101)
亞羣中發現:28
00:00:00.0017020(當沒有進行印刷測得)


越高k,越截斷可製成。這是當你看到主要的性能差異。當s變得更高時,您當前的代碼(遞歸版本)也會顯着變慢。

隨着k=5s=60
您當前的代碼:發現45070個亞羣2.8602923
我的代碼:發現45070個亞羣0.0116727
也就是說99.6%速度提高

+0

優秀的答案。非常感謝。你能否處理k = 0意味着任何子集大小的情況,並且你能否確認在最壞的情況下,這仍然在O(2^n)中運行? –

+0

如果需要,可以爲所有k運行一個循環。它運行得比O(2^n)好,我相信它是O(n^k)什麼的。 – SimpleVar

+0

我認爲它運行在O(2^k)而不是O(2^n),這是一個巨大的進步。 –

0

只需搜索大小爲K的所有組合,並檢查每個組合是否滿足條件。

最快的算法k組合,適合你的情況,應該是:

for (var i1 = 0; i1 <= n; i1++) 
{ 
    for (var i2 = i1 + 1; i2 <= n; i2++) 
    { 
     for (var i3 = i2 + 1; i3 <= n; i3++) 
     { 
      ... 

      for (var ik = iOneBeforeK + 1; ik <= n; ik++) 
      { 
       if (arr[i1] + arr[i2] + ... + arr[ik] == sum) 
       { 
        // this is a valid subset 
       } 
      } 
     } 
    } 
} 

但是你所談論的數字,總結起來,這意味着你可以使截止用更聰明的算法。

由於所有數字都是正數,因此您知道,如果單個數字足夠大,則無法再添加更多正數,並且總計爲s。鑑於s=6k=4,包含在搜索中的最高號碼是s-k+1=33+1+1+1k號碼,1是您可能的最低號碼,總計爲6。高於3的任何數字不能有3個其他正數添加到它,並且總和< = 6.

但是等待,您的最小可能值不是1,它是3。這甚至更好。所以假設k=10,n=60,min=3。 「最高數字場景」是x+min(k-1)=n - >x=60-3*9=33。所以甚至考慮的最高數字是33

這會削減數組中相關數字的數量,因此它會削減要查找的組合數量。

但它變得更好。假設k=10,n=60,min=3。陣列中的第一個數字恰好是20,所以它是相關的,應該檢查。讓我們找到相關的子集,包括20:
一個新的「難題」出現! k=10-1n=60-20min=3。您可以從subpuzzle現在截止許多號碼,一遍又一遍,每個一步。

這可以通過在subpuzzle計算k-1數最低的平均水平還要進一步加以改進和使用,作爲min

有可能在subpuzzle [0..n]預先計算k數最低平均水平還要進一步改善這一點,並k-1數最低平均subpuzzle [1..n],並在subpuzzle [2..n]k-2數最低平均等,而是使用它們重新計算的同樣的東西一次又一次地在每個子謎題評估中。

+0

謝謝你的回答,但我不能將袋子的尺寸從114更改爲只能剪下袋子的結果,但對於我所需要的原始114元袋子是不正確的。並且k-嵌套循環在編譯時是固定的,而k是運行時參數,再加上必須有比這更有效的方式。不管怎樣,謝謝你。 –

+0

@Ali這個答案是一項正在進行的工作。請檢查更新。 – SimpleVar

0

嘗試使用以下代碼。對不起,我沒有時間優化代碼。你可以比較你所有的方法,並做出更快的結論,不要忘記分享結果。

C#(你可以嘗試從列表中擺脫可能是它會給你的性能提升:

public static IEnumerable<List<int>> findSubsetsWithLengthKAndSumS2(Tuple<int, int> ks, List<int> set, List<int> subSet, List<int> subSetIndex) 
    { 
     if (ks.Item1 == 0 && ks.Item2 == 0) 
     { 
      var res = new List<List<int>>(); 
      res.Add(subSetIndex); 
      return res; 
     } 
     else if (ks.Item1 > 0 && ks.Item2 > 0) 
     { 
      var res = set.Select((x, i) => 
      { 
       var newSubset = subSet.Select(y => y).ToList(); 
       newSubset.Add(x); 

       var newSubsetIndex = subSetIndex.Select(y => y).ToList(); 
       newSubsetIndex.Add(i); 

       var newSet = set.Skip(i).ToList(); 
       return findSubsetsWithLengthKAndSumS2(Tuple.Create(ks.Item1 - 1, ks.Item2 - x), newSet, newSubset, newSubsetIndex).ToList(); 
      } 
      ).SelectMany(x => x).ToList(); 
      return res; 
     } 
     else 
      return new List<List<int>>(); 
    } 
... 
      var res = findSubsetsWithLengthKAndSumS2(Tuple.Create(2, 293), numbers.ToList(), new List<int>(), new List<int>()); 

F#(我加它只是爲了好玩=),它不是最優化過,我beleive最慢在代碼中的位置是'跳過'):

let skip (list:List<int>) index = 
    list |> List.mapi (fun i x -> if i > index then Some(x) else None) |> List.filter (fun x -> x.IsSome) |> List.map (fun x -> x.Value) 

let rec findSubsetsWithLengthKAndSumS (ks:int*int) (set:list<int>) (subSet:list<int>) = 
    [ 
     match ks with 
     |0,0 -> yield subSet 
     | x,y when x > 0 && y > 0 -> yield! set |> List.mapi (fun i x-> findSubsetsWithLengthKAndSumS ((fst ks)-1,(snd ks)-x) (skip set i) (x::subSet)) |> Seq.concat 
     | _,_-> yield [] 
    ] 
... 
    let res = Subsets.findSubsetsWithLengthKAndSumS (2,293) numbers [] |> List.filter (fun x-> x.Length >0) 

我相信這個迭代版本會比其他人在很多時候更快。它採用.NET 2.0

public delegate IEnumerable< IEnumerable<int> > findSubset(); 

    public delegate bool findSubsetsIterativeFilter(int[] sourceSet, int[] indiciesToSum, int expected); 

    public static bool Summ(int[] sourceSet, int[] indicies, int expected) 
    { 
     var sum = 0; 
     for(int i = 0; i < indicies.Length; i++) 
      sum += sourceSet[ indicies[ i ] ]; 
     return sum == expected; 
    } 

    public static IEnumerable< IEnumerable<int> > findSubsetsIterative(int k, int[] sourceSet, findSubsetsIterativeFilter specialCondition, int expected) 
    { 
     var a = new int[ k ]; 
     for(int i = 0; i < k; i++) 
      a[ i ] = i; 

     var p = k - 1; 
     while(p >= 0) 
     { 
      if(specialCondition(sourceSet, a, expected)) 
       yield return (int[])a.Clone(); 
      p = (a[ k - 1 ] == sourceSet.Length - 1) ? p - 1 : k - 1; 
      if(p >= 0) 
       for(int i = k - 1; i >= p; i--) 
        a[ i ] = a[ p ] + i - p + 1; 
     } 
    } 
    ... 
     findSubsetsIterative(2, a, Summ, 293); 

我測量了我的代碼和Yorye的,這裏是我所得到的。我的代碼在4-10x中速度更快。您是否在實驗中使用了我的答案中的「findSubsetsIterative」?

findSubsetsIterative(4,GenerateSOurceData(1),薩姆,400)經過的: 00:00:00.0012113 puzzle.Solve(4,400,PuzzleOnSubsetFound)經過的: 00:00:00.0046170

findSubsetsIterative (5,GenerateSOurceData(1),薩姆,60)經過的: 00:00:00.0012960 puzzle.Solve(5,60,PuzzleOnSubsetFound)經過的: 00:00:00.0108568

在這裏,我在5倍增加的傳入陣列(剛剛將數組複製5次到新的大數組中): findSubsetsIterative(5, GenerateSOurceData(5),薩姆,60) 消逝:00:00:00.0013067 puzzle.Solve(5,60,PuzzleOnSubsetFound) 消逝:00:00:21.3520840

+0

我不敢使用Linq我很害怕。必須堅持使用[]數組只能使用.NET 2.0,但List 也是可以接受的。感謝您的代碼,我希望其他人也會發現它也很有用。 –

+0

好吧,我已經添加了可以在.net 2.0上運行的迭代版本,並且這種方法在數百次中更快。這是因爲它具有低層次的數據結構實現並且具有較低的時間複雜度。它適合你嗎? – burzhuy

+0

非常感謝您的努力。我會測試它並報告回來。 –

相關問題