用于弦切割的动态编程练习

时间:2012-04-09 03:25:55

标签: algorithm dynamic-programming

我一直在研究book中的以下问题。

  

某种字符串处理语言提供了一种原始操作,它将字符串分成两部分。由于此操作涉及复制原始字符串,因此无论剪切的位置如何,对于长度为n的字符串,都需要n个时间单位。现在假设您要将字符串分成许多部分。中断的顺序可能会影响总运行时间。例如,如果你想在3号和10号位置剪切一个20个字符的字符串,那么在第3个位置进行第一次剪切会产生20 + 17 = 37的总成本,而在第10个位置进行第一次剪切会产生更好的成本20+ 10 = 30。

我需要一个动态编程算法,给出m个切割,找到将字符串切割成m + 1个片段的最低成本。

4 个答案:

答案 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

** 修改 **

我已经实现了另外两种方法来回答问题中的问题。

1。中位数近似值

这种方法递归地切割最大的块。结果并不总是最好的解决方案,但提供了一个不可忽略的增益(从我的测试中获得+ 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;
}

2。恒定时间多次切割

而不是计算最小成本,只需使用不同的索引和缓冲区。由于此方法在恒定时间内执行,因此它始终返回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;
}