如何理解线性分区中的动态规划解决方案?
Posted
技术标签:
【中文标题】如何理解线性分区中的动态规划解决方案?【英文标题】:How to understand the dynamic programming solution in linear partitioning? 【发布时间】:2011-12-17 20:05:34 【问题描述】:我很难理解线性分区问题的动态编程解决方案。我正在阅读Algorithm Design Manual,问题在第 8.5 节中描述。我已经无数次阅读该部分,但我就是不明白。我认为这是一个糟糕的解释(我到目前为止所读的内容要好得多),但我无法很好地理解这个问题,无法寻找替代解释。欢迎提供更好解释的链接!
我找到了一个与本书类似的页面(可能来自本书的第一版):The Partition Problem。
第一个问题:在书中的示例中,分区是从最小到最大排序的。这只是巧合吗?据我所知,元素的顺序对算法并不重要。
这是我对递归的理解:
让我们使用以下序列并将其划分为 4:
S1...Sn = 100 150 200 250 300 350 400 450 500
k = 4
第二个问题:我认为递归将如何开始 - 我理解正确吗?
第一个递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 350 | 400 | 450 | 500 //1 partition to go
100 150 200 250 300 | 350 | 400 | 450 | 500 //done
第二次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 350 | 400 | 450 | 500 //1 partition to go
100 150 200 250 | 300 350 | 400 | 450 | 500 //done
第三次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 350 | 400 | 450 | 500 //1 partition to go
100 150 200 | 250 300 350 | 400 | 450 | 500 //done
第四次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 350 | 400 | 450 | 500 //1 partition to go
100 150 | 200 250 300 350 | 400 | 450 | 500 //done
第5次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 350 | 400 | 450 | 500 //1 partition to go
100 | 150 200 250 300 350 | 400 | 450 | 500 //done
第6次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 | 350 400 | 450 | 500 //1 partition to go
100 150 200 250 | 300 | 350 400 | 450 | 500 //done
第7次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 | 350 400 | 450 | 500 //1 partition to go
100 150 200 | 250 300 | 350 400 | 450 | 500 //done
第8次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 | 350 400 | 450 | 500 //1 partition to go
100 150 | 200 250 300 | 350 400 | 450 | 500 //done
第9次递归是:
100 150 200 250 300 350 400 450 | 500 //3 partition to go
100 150 200 250 300 350 400 | 450 | 500 //2 partition to go
100 150 200 250 300 | 350 400 | 450 | 500 //1 partition to go
100 | 150 200 250 300 | 350 400 | 450 | 500 //done
等等……
这是书中出现的代码:
partition(int s[], int n, int k)
int m[MAXN+1][MAXK+1]; /* DP table for values */
int d[MAXN+1][MAXK+1]; /* DP table for dividers */
int p[MAXN+1]; /* prefix sums array */
int cost; /* test split cost */
int i,j,x; /* counters */
p[0] = 0; /* construct prefix sums */
for (i=1; i<=n; i++) p[i]=p[i-1]+s[i];
for (i=1; i<=n; i++) m[i][3] = p[i]; /* initialize boundaries */
for (j=1; j<=k; j++) m[1][j] = s[1];
for (i=2; i<=n; i++) /* evaluate main recurrence */
for (j=2; j<=k; j++)
m[i][j] = MAXINT;
for (x=1; x<=(i-1); x++)
cost = max(m[x][j-1], p[i]-p[x]);
if (m[i][j] > cost)
m[i][j] = cost;
d[i][j] = x;
reconstruct_partition(s,d,n,k); /* print book partition */
关于算法的问题:
m
和 d
中存储了哪些值?
“成本”是什么意思?它只是分区内元素值的总和吗?还是有其他更微妙的含义?
【问题讨论】:
顺便说一句,即使你不能回答我的问题,我也会感谢 cmets 对源材料的质量。我想确认一下,不只是我觉得解释很糟糕(这让我感到相当愚蠢)。 我不认为你会发现很多人在这里能够回答你的问题,而不对你需要解决的问题给出简洁的解释。分区问题有很多变体,粘贴手动执行的算法的长表并不能使事情更清晰。 【参考方案1】:请注意,书中对算法的解释有一个小错误,请查看errata中的文字“(*)第297页”。
关于您的问题:
-
不,项目不需要排序,只需要连续(即不能重新排列)
我认为可视化算法的最简单方法是手动跟踪
reconstruct_partition
过程,使用图 8.8 中最右边的表格作为指导
在书中指出 m[i][j] 是“s1, s2, ... , si 的所有分区的最小可能成本”到 j 个范围内,其中分区的成本是其一个部分中元素的最大总和”。换句话说,它是“最小的最大总和”,如果你原谅术语的滥用。另一方面, d[i][j] 存储索引位置,它是用于为之前定义的给定对 i,j 进行分区
“成本”的含义见上一个回答
编辑:
这是我对线性分区算法的实现。它基于 Skiena 的算法,但是以 Python 的方式;它会返回一个分区列表。
from operator import itemgetter
def linear_partition(seq, k):
if k <= 0:
return []
n = len(seq) - 1
if k > n:
return map(lambda x: [x], seq)
table, solution = linear_partition_table(seq, k)
k, ans = k-2, []
while k >= 0:
ans = [[seq[i] for i in xrange(solution[n-1][k]+1, n+1)]] + ans
n, k = solution[n-1][k], k-1
return [[seq[i] for i in xrange(0, n+1)]] + ans
def linear_partition_table(seq, k):
n = len(seq)
table = [[0] * k for x in xrange(n)]
solution = [[0] * (k-1) for x in xrange(n-1)]
for i in xrange(n):
table[i][0] = seq[i] + (table[i-1][0] if i else 0)
for j in xrange(k):
table[0][j] = seq[0]
for i in xrange(1, n):
for j in xrange(1, k):
table[i][j], solution[i-1][j-1] = min(
((max(table[x][j-1], table[i][0]-table[x][0]), x) for x in xrange(i)),
key=itemgetter(0))
return (table, solution)
【讨论】:
谢谢,这有助于我得出结论。1,1,1,1,1,1,1,1,1
运行代码的结果是1,1,1,1,1|1|1,1
,根据文本应该是1,1,1|1,1,1|1,1,1
。我有可能误解了输出。如果是这种情况,我将其归咎于糟糕的写作,而不是因为我不想尝试。鉴于这本书收到了如此多的好评,我对此感到惊讶。seq=[20, 20, 20, 110, 15, 7, 10, 20, 20, 10, 20, 10, 20, 20, 10, 15, 20, 15, 7]
、k=6
。 result=[[20], [20, 20], [110], [15], [7, 10, 20, 20, 10, 20, 10], [20, 20, 10, 15, 20, 15, 7]]
。在单个 110 元素(正确)之后是单个 15(错误)。【参考方案2】:
我已经在 php 上实现了 Óscar López 算法。请随时在需要时使用它。
/**
* Example: linear_partition([9,2,6,3,8,5,8,1,7,3,4], 3) => [[9,2,6,3],[8,5,8],[1,7,3,4]]
* @param array $seq
* @param int $k
* @return array
*/
protected function linear_partition(array $seq, $k)
if ($k <= 0)
return array();
$n = count($seq) - 1;
if ($k > $n)
return array_map(function ($x)
return array($x);
, $seq);
list($table, $solution) = $this->linear_partition_table($seq, $k);
$k = $k - 2;
$ans = array();
while ($k >= 0)
$ans = array_merge(array(array_slice($seq, $solution[$n - 1][$k] + 1, $n - $solution[$n - 1][$k])), $ans);
$n = $solution[$n - 1][$k];
$k = $k - 1;
return array_merge(array(array_slice($seq, 0, $n + 1)), $ans);
protected function linear_partition_table($seq, $k)
$n = count($seq);
$table = array_fill(0, $n, array_fill(0, $k, 0));
$solution = array_fill(0, $n - 1, array_fill(0, $k - 1, 0));
for ($i = 0; $i < $n; $i++)
$table[$i][0] = $seq[$i] + ($i ? $table[$i - 1][0] : 0);
for ($j = 0; $j < $k; $j++)
$table[0][$j] = $seq[0];
for ($i = 1; $i < $n; $i++)
for ($j = 1; $j < $k; $j++)
$current_min = null;
$minx = PHP_INT_MAX;
for ($x = 0; $x < $i; $x++)
$cost = max($table[$x][$j - 1], $table[$i][0] - $table[$x][0]);
if ($current_min === null || $cost < $current_min)
$current_min = $cost;
$minx = $x;
$table[$i][$j] = $current_min;
$solution[$i - 1][$j - 1] = $minx;
return array($table, $solution);
【讨论】:
【参考方案3】:以下是Skienna的线性分区算法在python中的修改实现,除了答案本身之外不计算最后k列的值:M[N][K](一个单元格计算只取决于前一个)
对输入 1,2,3,4,5,6,7,8,9(在书中 Skienna 的示例中使用)的测试产生了一个稍微不同的矩阵 M(鉴于上述修改)但正确返回最终结果(在本例中,将 s 划分为 k 个范围的最小成本为 17,矩阵 D 用于打印导致该最优值的分隔符位置列表)。
import math
def partition(s, k):
# compute prefix sums
n = len(s)
p = [0 for _ in range(n)]
m = [[0 for _ in range(k)] for _ in range(n)]
d = [[0 for _ in range(k)] for _ in range(n)]
for i in range(n):
p[i] = p[i-1] + s[i]
# initialize boundary conditions
for i in range(n):
m[i][0] = p[i]
for i in range(k):
m[0][i] = s[0]
# Evaluate main recurrence
for i in range(1, n):
"""
omit calculating the last M's column cells
except for the sought minimum cost M[N][K]
"""
if i != n - 1:
jlen = k - 1
else:
jlen = k
for j in range(1, jlen):
"""
- computes the minimum-cost partitioning of the set S1,S2,.., Si into j partitions .
- this part should be investigated more closely .
"""
#
m[i][j] = math.inf
# This loop needs to be traced to understand it better
for x in range(i):
sup = max(m[x][j-1], p[i] - p[x])
if m[i][j] > sup:
m[i][j] = sup
# record which divider position was required to achieve the value s
d[i][j] = x+1
return s, d, n, k
def reconstruct_partition(S, D, N, K):
if K == 0:
for i in range(N):
print(S[i], end="_")
print(" | ", end="")
else:
reconstruct_partition(S, D, D[N-1][K-1], K-1)
for i in range(D[N-1][K-1], N):
print(S[i], end="_")
print(" | ", end="")
# MAIN PROGRAM
S, D, N, K = partition([1, 2, 3, 4, 5, 6, 7, 8, 9], 3)
reconstruct_partition(S, D, N, K)
【讨论】:
以上是关于如何理解线性分区中的动态规划解决方案?的主要内容,如果未能解决你的问题,请参考以下文章
动态规划中的0-1背包问题怎么去理解?要求给出具体实例和详细步骤。。。