我一直在研究book中的以下问题。
某种字符串处理语言提供了一种原始操作,它将字符串分成两部分。由于此操作涉及复制原始字符串,因此无论剪切的位置如何,对于长度为n的字符串,都需要n个时间单位。现在假设您要将字符串分成许多部分。中断的顺序可能会影响总运行时间。例如,如果你想在3号和10号位置剪切一个20个字符的字符串,那么在第3个位置进行第一次剪切会产生20 + 17 = 37的总成本,而在第10个位置进行第一次剪切会产生更好的成本20+ 10 = 30。
我需要一个动态编程算法,给出m个切割,找到将字符串切割成m + 1个片段的最低成本。
答案 0 :(得分:3)
对于动态编程,我声称你真正需要知道的是状态空间应该是什么 - 如何表示部分问题。
这里我们通过创建新的中断将一个字符串分成m + 1个。我声称一个好的状态空间是一组(a,b)对,其中a是子字符串开头的位置,b是同一子字符串结尾的位置,计为最终的断点数打破了字符串。与每对相关的成本是分解它的最低成本。如果b <= a + 1,那么成本为0,因为没有更多的中断。如果b更大,那么该子串中下一个中断的可能位置是点a + 1,a + 2,... b-1。无论我们把它放在哪里,下一次休息将花费b-a,但如果我们把它放在位置k,后来休息的最低成本是(a,k)+(k,b)。
所以要通过动态编程来解决这个问题,建立一个最低成本的表(a,b),你可以通过考虑k-1个可能的中断来计算k个部分的字符串中断成本,然后查找字符串成本最多为k-1个部分。
扩展此方法的一种方法是首先创建一个表T [a,b]并将该表中的所有条目设置为无穷大。然后再次遍历表格,其中b&lt; = a + 1将T [a,b] = 0。这填写表示原始字符串的部分的条目,不需要进一步剪切。现在扫描表格,并扫描每个T [a,b],其中b> a + 1考虑每个可能的k,使得a <1。 k&lt; b,如果min_k((断点a和b之间的长度)+ T [a,k] + T [k,b])&lt; T [a,b]将T [a,b]设置为该最小值。这可以识别出你现在知道如何廉价地切断由T [a,k]和T [k,b]表示的子串的方法,因此这为你提供了一种更好的方法来切断T [a,b]。如果你现在重复这几次,你就完成了 - 使用标准的动态编程回溯来计算出解决方案。如果在单独的表中为每个T [a,b]保存k的最佳值,可能会有所帮助。
答案 1 :(得分:2)
对我来说,分而治之的方法对于这类问题来说是最好的方法。这是算法的Java实现:
注意:数组m
应按升序排序(使用Arrays.sort(m);
)
public int findMinCutCost(int[] m, int n) {
int cost = n * m.length;
for (int i=0; i<m.length; i++) {
cost = Math.min(findMinCutCostImpl(m, n, i), cost);
}
return cost;
}
private int findMinCutCostImpl(int[] m, int n, int i) {
if (m.length == 1) return n;
int cl = 0, cr = 0;
if (i > 0) {
cl = Integer.MAX_VALUE;
int[] ml = Arrays.copyOfRange(m, 0, i);
int nl = m[i];
for (int j=0; j<ml.length; j++) {
cl = Math.min(findMinCutCostImpl(ml, nl, j), cl);
}
}
if (i < m.length - 1) {
cr = Integer.MAX_VALUE;
int[] mr = Arrays.copyOfRange(m, i + 1, m.length);
int nr = n - m[i];
for (int j=0; j<mr.length; j++) {
mr[j] = mr[j] - m[i];
}
for (int j=0; j<mr.length; j++) {
cr = Math.min(findMinCutCostImpl(mr, nr, j), cr);
}
}
return n + cl + cr;
}
例如:
int n = 20;
int[] m = new int[] { 10, 3 };
System.out.println(findMinCutCost(m, n));
将打印30
** 修改 **
我已经实现了另外两种方法来回答问题中的问题。
这种方法递归地切割最大的块。结果并不总是最好的解决方案,但提供了一个不可忽略的增益(从我的测试中获得+ 100000%的增益),与最佳成本相比可以忽略不计的最小切割损失。
public int findMinCutCost2(int[] m, int n) {
if (m.length == 0) return 0;
if (m.length == 1) return n;
float half = n/2f;
int bestIndex = 0;
for (int i=1; i<m.length; i++) {
if (Math.abs(half - m[bestIndex]) > Math.abs(half - m[i])) {
bestIndex = i;
}
}
int cl = 0, cr = 0;
if (bestIndex > 0) {
int[] ml = Arrays.copyOfRange(m, 0, bestIndex);
int nl = m[bestIndex];
cl = findMinCutCost2(ml, nl);
}
if (bestIndex < m.length - 1) {
int[] mr = Arrays.copyOfRange(m, bestIndex + 1, m.length);
int nr = n - m[bestIndex];
for (int j=0; j<mr.length; j++) {
mr[j] = mr[j] - m[bestIndex];
}
cr = findMinCutCost2(mr, nr);
}
return n + cl + cr;
}
而不是计算最小成本,只需使用不同的索引和缓冲区。由于此方法在恒定时间内执行,因此它始终返回n。另外,方法实际在子串中拆分字符串。
public int findMinCutCost3(int[] m, int n) {
char[][] charArr = new char[m.length+1][];
charArr[0] = new char[m[0]];
for (int i=0, j=0, k=0; j<n; j++) {
//charArr[i][k++] = string[j]; // string is the actual string to split
if (i < m.length && j == m[i]) {
if (++i >= m.length) {
charArr[i] = new char[n - m[i-1]];
} else {
charArr[i] = new char[m[i] - m[i-1]];
}
k=0;
}
}
return n;
}
注意:可以轻松修改最后一个方法以接受String str
参数而不是n
并设置n = str.length()
,并返回{{1来自String[]
的数组。
答案 2 :(得分:1)
python代码:
mincost(n, cut_list) =min { n+ mincost(k,left_cut_list) + min(n-k, right_cut_list) }
import sys
def splitstr(n,cut_list):
if len(cut_list) == 0:
return [0,[]]
min_positions = []
min_cost = sys.maxint
for k in cut_list:
left_split = [ x for x in cut_list if x < k]
right_split = [ x-k for x in cut_list if x > k]
#print n,k, left_split, right_split
lcost = splitstr(k,left_split)
rcost = splitstr(n-k,right_split)
cost = n+lcost[0] + rcost[0]
positions = [k] + lcost[1]+ [x+k for x in rcost[1]]
#print "cost:", cost, " min: ", positions
if cost < min_cost:
min_cost = cost
min_positions = positions
return ( min_cost, min_positions)
print splitstr(20,[3,10,16]) # (40, [10, 3, 16])
print splitstr(20,[3,10]) # (30, [10, 3])
print splitstr(5,[1,2,3,4,5]) # (13, [2, 1, 3, 4, 5])
print splitstr(1,[1]) # (1, [1]) # m cuts m+1 substrings
答案 3 :(得分:0)
这是一个c ++实现。它是使用D.P的O(n ^ 3)实现。假设切割数组已排序。如果不是,则需要O(n ^ 3)时间对其进行排序,因此渐近时间复杂度保持不变。
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <limits.h>
using namespace std;
int main(){
int i,j,gap,k,l,m,n;
while(scanf("%d%d",&n,&k)!=EOF){
int a[n+1][n+1];
int cut[k];
memset(a,0,sizeof(a));
for(i=0;i<k;i++)
cin >> cut[i];
for(gap=1;gap<=n;gap++){
for(i=0,j=i+gap;j<=n;j++,i++){
if(gap==1)
a[i][j]=0;
else{
int min = INT_MAX;
for(m=0;m<k;m++){
if(cut[m]<j and cut[m] >i){
int cost=(j-i)+a[i][cut[m]]+a[cut[m]][j];
if(cost<min)
min=cost;
}
}
if(min>=INT_MAX)
a[i][j]=0;
else
a[i][j]=min;
}
}
}
cout << a[0][n] << endl;
}
return 0;
}