【问题标题】:Array partition using dynamic programming使用动态规划的数组分区
【发布时间】:2019-04-14 19:50:00
【问题描述】:

我应该对两个分区问题的动态规划实现进行什么修改来解决以下任务:

给定一个正整数数组作为输入,记为 C。程序应决定是否可以将数组划分为两个相等的子序列。您可以从数组中删除一些元素,但不是全部,以使这样的分区可行。

示例:

假设输入是 4 5 11 17 9。如果我们删除 11 和 17,则可能有两个分区。我的问题是我应该对我的两个分区实现进行哪些调整以确定两个分区是否可能(可能需要也可能不需要删除某些元素)或输出即使删除某些元素也是不可能的两个分区。程序应该在 O(sum^2 * C) 时间内运行。

这是我在 Python 中的两个分区实现:

def two_partition(C):
    n = len(C)
    s = sum(C)

    if s % 2 != 0: return False

    T = [[False for _ in range(n + 1)] for _ in range(s//2 + 1)]
    for i in range(n + 1): T[0][i] = True

    for i in range(1, s//2 + 1):
        for j in range(1, n + 1):
            T[i][j] = T[i][j-1]
            if i >= C[j-1]:
                T[i][j] = T[i][j] or T[i-C[j-1]][j-1]

    return T[s // 2][n]

【问题讨论】:

  • 首先要确定您的桌子的尺寸。您当前的算法需要 C*s/2 表,复杂度为 O(C*s)。所需的算法应该具有 O(C*s*s) 复杂度,所以...
  • 请说明 [2, 3, 1] 的预期输出以及原因。
  • @גלעדברקן 预期的输出是 {2,1} 和 {3} 因此可以将数组划分为两个相等的子数组。在这种情况下,我们不需要删除任何元素。
  • 在这种情况下,我会将“子数组”一词更改为“子集”或“子序列”。我认为子数组主要被理解为连续的。
  • @גלעדברקן 完成。

标签: python-3.x algorithm dynamic-programming


【解决方案1】:

要确定是否可行,请在两个部分之间保留一组独特的差异。对于每个元素,迭代目前看到的差异;减去和添加元素。我们正在寻找差异 0。

4 5 11 17 9

0 (empty parts)

|0 ± 4| = 4

set now has 4 and empty-parts-0

|0 ± 5| = 5
|4 - 5| = 1
|4 + 5| = 9

set now has 4,5,1,9 and empty-parts-0

|0 ± 11| = 11
|4 - 11| = 7
|4 + 11| = 15
|5 - 11| = 6
|5 + 11| = 16
|1 - 11| = 10
|1 + 11| = 12
|9 - 11| = 2
|9 + 11| = 20

... (iteration with 17)

|0 ± 9| = 9
|4 - 9| = 5
|4 + 9| = 13
|5 - 9| = 4
|5 + 9| = 14
|1 - 9| = 8
|1 + 9| = 10
|9 - 9| = 0

Bingo!

Python 代码:

def f(C):
  diffs = set()

  for n in C:
    new_diffs = [n]

    for d in diffs:
      if d - n == 0:
        return True
      new_diffs.extend([abs(d - n), abs(d + n)])

    diffs = diffs.union(new_diffs)

  return False

输出:

> f([2, 3, 7, 2])

=> True

> f([2, 3, 7])

=> False

> f([7, 1000007, 1000000])

=> True

【讨论】:

  • 首选动态编程解决方案。还请证明您的解决方案的合理性,即您的算法为何有效和运行时间。
  • @user1812 这对我来说似乎是动态编程——重叠的子问题是两个不相交的子集之间可实现的差异。它之所以有效,是因为它试图将每个元素放在一个子集 (-) 或另一个 (+) 中,并在所见差异集中留下两者的选择。运行时间为 O(|C| * number-of-achievable-differences)。
【解决方案2】:

创建一个 3 维数组,以第一个分区的总和、第二个分区的总和和元素数为索引。 T[i][j][k] 仅当在第一个 k 元素中可能有两个不相交的子集,其总和分别为 ij 时才为真。

要计算它,您需要考虑每个元素的三种可能性。它要么出现在第一组或第二组中,要么完全被删除。 在循环中为每个可能的 sum 组合执行此操作会在 O(sum ^ 2 * C) 中生成所需的数组。

要找到您问题的答案,您需要检查的是有一些总和 i 使得 T[i][i][n] 为真。这意味着有两个不同的子集,它们的总和为i,正如问题所要求的那样。

如果您需要找到实际的子集,可以使用简单的回溯功能轻松完成。只需检查 back_track 函数和递归中的三种可能性中的哪一种。

这是一个示例实现:

def back_track(T, C, s1, s2, i):
    if s1 == 0 and s2 == 0: return [], []
    if T[s1][s2][i-1]:
        return back_track(T, C, s1, s2, i-1)
    elif s1 >= C[i-1] and T[s1 - C[i-1]][s2][i-1]:
        a, b = back_track(T, C, s1 - C[i-1], s2, i-1)
        return ([C[i-1]] + a, b)
    else:
        a, b = back_track(T, C, s1, s2 - C[i-1], i-1)
        return (a, [C[i-1]] + b)

def two_partition(C):
    n = len(C)
    s = sum(C)

    T = [[[False for _ in range(n + 1)] for _ in range(s//2 + 1)] for _ in range(s // 2 + 1)]
    for i in range(n + 1): T[0][0][i] = True

    for s1 in range(0, s//2 + 1):
        for s2 in range(0, s//2 + 1):
            for j in range(1, n + 1):
                T[s1][s2][j] = T[s1][s2][j-1]
                if s1 >= C[j-1]:
                    T[s1][s2][j] = T[s1][s2][j] or T[s1-C[j-1]][s2][j-1]
                if s2 >= C[j-1]:
                    T[s1][s2][j] = T[s1][s2][j] or T[s1][s2-C[j-1]][j-1]
    for i in range(1, s//2 + 1):
        if T[i][i][n]:
            return back_track(T, C, i, i, n)
    return False

print(two_partition([4, 5, 11, 9]))
print(two_partition([2, 3, 1]))
print(two_partition([2, 3, 7]))

【讨论】:

  • @גלעדברקן 哎呀,函数返回真或假,所以我完全错过了那部分。通过回溯成功的 i 和 j 可以轻松完成。如果 T1[i][j-1] 也为真,则跳过第 j 个元素,否则需要它。递减 j 直到达到 0。对 T2 反向应用相同的过程。
  • 只是想澄清一下T2的作用是什么以及为什么最后一个循环保证两个分区是可能的。能简单解释一下吗?
  • @user1812 它没有。此函数为 [2,3,1] 返回 False。这个想法被打破了,无法修复。您的复杂性估计是正确的。
  • @n.m.但是,对于 [2, 3, 1],答案应该是错误的。分区应该在两个子数组中,而不是子集。 [2, 3, 1] 怎么可能?
  • @user1812 最后一个循环保证有一些总和 i,这样你就可以在前 j 个元素中找到一个子集,以及从 j+1 开始的一个子集,这两个元素都等于 i。删除这两个子集以外的所有元素将导致两个子数组的和相等。
【解决方案3】:

我迅速调整了代码,用于搜索给定问题的三个等和子集。

算法尝试将每个项目A[idx] 放入第一个袋子,或第二个袋子(两者都是真实袋子)或第三个(假)袋子(忽略的项目)。实际袋子中的初始值(可用空间)是总和的一半。这种方法原样具有指数复杂性(具有 3^N 个叶子的决策树)

但是有很多重复分布,所以我们可以记住一些状态而没有机会忽略分支,所以使用了一种 DP - memoization。这里提到的状态是当我们使用从最后一个索引到 idx 的项目时,真实包中的可用空间集。

状态存储的可能大小可能达到N * sum/2 * sum/2

可工作的 Delphi 代码(未经彻底测试,似乎有一个忽略项目输出的错误)

function Solve2(A: TArray<Integer>): string;
var
  Map: TDictionary<string, boolean>;
  Lists: array of TStringList;
  found: Boolean;
  s2: integer;

function CheckSubsetsWithItem(Subs: TArray<Word>; idx: Int16): boolean;
var
  key: string;
  i: Integer;
begin
    if (Subs[0] = Subs[1]) and (Subs[0] <> s2) then begin
      found:= True;
      Exit(True);
    end;

    if idx < 0 then
      Exit(False);

   //debug map contains current rests of sums in explicit representation

    key := Format('%d_%d_%d', [subs[0], subs[1], idx]);

    if  Map.ContainsKey(key) then
       //memoisation
       Result := Map.Items[key]
    else begin
        Result := false;
        //try to put A[idx] into the first, second bag or ignore it
        for i := 0 to 2 do begin
           if Subs[i] >= A[idx] then begin
            Subs[i] := Subs[i] - A[idx];
            Result := CheckSubsetsWithItem(Subs, idx - 1);
            if Result  then begin
              //retrieve subsets themselves at recursion unwindning
              if found then
                 Lists[i].Add(A[idx].ToString);
              break;
            end
            else
              //reset sums before the next try
              Subs[i] := Subs[i] + A[idx];
           end;
        end;
        //remember result - memoization
        Map.add(key, Result);
    end;
end;


var
  n, sum: Integer;
  Subs: TArray<Word>;
begin
  n := Length(A);
  sum := SumInt(A);
  s2 := sum div 2;
  found := False;

  Map := TDictionary<string, boolean>.Create;
  SetLength(Lists, 3);
  Lists[0] := TStringList.Create;
  Lists[1] := TStringList.Create;
  Lists[2] := TStringList.Create;

   if CheckSubsetsWithItem([s2, s2, sum], n - 1) then begin
     Result := '[' + Lists[0].CommaText + '], ' +
               '[' + Lists[1].CommaText + '], ' +
               ' ignored: [' + Lists[2].CommaText + ']';
   end else
     Result := 'No luck :(';
end;



begin
   Memo1.Lines.Add(Solve2([1, 5, 4, 3, 2, 16,21,44, 19]));
   Memo1.Lines.Add(Solve2([1, 3, 9, 27, 81, 243, 729, 6561]));
end;

[16,21,19], [1,5,4,2,44],  ignored: [3]

No luck :(

【讨论】:

    猜你喜欢
    • 2011-09-23
    • 1970-01-01
    • 2020-06-20
    • 1970-01-01
    • 2013-08-01
    • 2012-11-12
    • 2012-10-29
    • 1970-01-01
    • 2014-04-07
    相关资源
    最近更新 更多