【问题标题】:Word wrap to X lines instead of maximum width (Least raggedness)自动换行到 X 行而不是最大宽度(最不粗糙)
【发布时间】:2011-06-21 13:28:03
【问题描述】:

有没有人知道将输入字符串换行到指定行数而不是设定宽度的好算法。基本上达到X线的最小宽度。

e.g. "I would like to be wrapped into two lines"
goes to
"I would like to be
wrapped into two lines"

"I would like to be wrapped into three lines"
goes to
"I would like to
be wrapped into
three lines"

根据需要插入新行。我可以找到其他自动换行问题,但它们都有一个已知的宽度,并且想要插入尽可能多的行以适应该宽度。我正好相反。

最好用 .NET 语言回答,但任何语言都会有帮助。显然,如果有一个框架方法可以做到这一点,我不知道让我知道。

编辑我发现了这个,我认为接受的答案是我的问题的解决方案,但我很难理解它。 Algorithm to divide text into 3 evenly-sized groups 任何人都可以将其转换为 c# 或 vb.net。

【问题讨论】:

  • 您追求的是最优解还是接近最优解是否足够好?允许连字吗?
  • 理想情况下是最优的,但我会欢迎一个接近最优的答案。我可以看到一种贪婪的方法,它涉及一组单词,然后将总长度除以行数作为要中断的值。但是我可以看到它并不总是最好的,这可能是你也在想的。我试图避免断字。
  • 假设该行有 30 个字符长,并且应该包含 3 行,最佳解决方案是 3 行长度为 10。我们现在可以搜索行数最多的解决方案长度为 10,或与 10 的差异最小的解决方案。最好用 100 个字符和 10 行显示:10、10、10、10 ...、10、19、1 是一个好的解决方案,因为它是 8 倍10 完美,与下面的示例相比,这是永远不正确的,或者是 11, 9, 11, 9, ... 11, 9 更好的解决方案,因为最大差异是 1,与上面示例中的 9 相比?
  • 啊,也许我做了一个假设。是的,首选的解决方案是 11、9、11、9... 的想法是将最长的线保持在最低限度
  • 我将python解决方案转换为C#。看我的回答。很有趣:)

标签: algorithm word-wrap


【解决方案1】:

解决此问题的一种方法是使用动态规划,您可以使用动态规划解决此问题,参见Minimum raggedness algorithm。 我使用了您在编辑帖子时添加的一些信息: Algorithm to divide text into 3 evenly-sized groups


符号:

让你的文本命名为 document="word1 word2 .... wordp"

n= 所需行数

LineWidth=len(文档)/n


成本函数:

首先你需要定义一个在同一行中有word[i]到word[j]的代价函数,你可以取和wikipedia上的一样,例如p=2:

表示直线的目标长度与实际长度之间的距离。

最优解的总成本函数可以用以下递归关系定义:


解决问题:

您可以使用动态规划解决此问题。 我从您提供的链接中获取了代码,并对其进行了更改,以便您查看程序正在使用什么。

  1. 在第 k 阶段,您将单词添加到第 k 行。
  2. 然后你看看最优成本 在第 k 行有单词 i 到 j。
  3. 从第 1 行转到第 n 行后, 你采取最小的成本 最后一步,你有你的最佳 结果:

这是代码的结果:

D=minragged('Just testing to see how this works.')

number of words: 7
------------------------------------
stage : 0
------------------------------------
word i to j in line 0       TotalCost (f(j))
------------------------------------
i= 0 j= 0           121.0
i= 0 j= 1           49.0
i= 0 j= 2           1.0
i= 0 j= 3           16.0
i= 0 j= 4           64.0
i= 0 j= 5           144.0
i= 0 j= 6           289.0
i= 0 j= 7           576.0
------------------------------------
stage : 1
------------------------------------
word i to j in line 1       TotalCost (f(j))
------------------------------------
i= 0 j= 0           242.0
i= 0 j= 1           170.0
i= 0 j= 2           122.0
i= 0 j= 3           137.0
i= 0 j= 4           185.0
i= 0 j= 5           265.0
i= 0 j= 6           410.0
i= 0 j= 7           697.0
i= 1 j= 2           65.0
i= 1 j= 3           50.0
i= 1 j= 4           58.0
i= 1 j= 5           98.0
i= 1 j= 6           193.0
i= 1 j= 7           410.0
i= 2 j= 4           26.0
i= 2 j= 5           2.0
i= 2 j= 6           17.0
i= 2 j= 7           122.0
i= 3 j= 7           80.0
------------------------------------
stage : 2
------------------------------------
word i to j in line 2       TotalCost (f(j))
------------------------------------
i= 0 j= 7           818.0
i= 1 j= 7           531.0
i= 2 j= 7           186.0
i= 3 j= 7           114.0
i= 4 j= 7           42.0
i= 5 j= 7           2.0
reversing list
------------------------------------
Just testing        12
to see how      10
this works.         11
  • *因此,最好的选择是在最后一行包含单词 5 到 7。(参见 阶段2)
  • 然后是第二行中的单词 2 到 5(参见 阶段1)
  • 然后是第一行中的单词 0 到 2(参见 阶段 0).*

反过来,你会得到:

Just testing          12
to see how          10
this works.          11

这是打印推理的代码,(在 python 中对不起我不使用 C#...但我实际上有人用 C# 翻译了代码):

def minragged(text, n=3):


    P=2
    words = text.split()
    cumwordwidth = [0]
    # cumwordwidth[-1] is the last element
    for word in words:
        cumwordwidth.append(cumwordwidth[-1] + len(word))
    totalwidth = cumwordwidth[-1] + len(words) - 1  # len(words) - 1 spaces
    linewidth = float(totalwidth - (n - 1)) / float(n)  # n - 1 line breaks

    print "number of words:", len(words)
    def cost(i, j):
        """
        cost of a line words[i], ..., words[j - 1] (words[i:j])
        """
        actuallinewidth = max(j - i - 1, 0) + (cumwordwidth[j] - cumwordwidth[i])
        return (linewidth - float(actuallinewidth)) ** P

    """
    printing the reasoning and reversing the return list
    """
    F={} # Total cost function

    for stage in range(n):
        print "------------------------------------"
        print "stage :",stage
        print "------------------------------------"
        print "word i to j in line",stage,"\t\tTotalCost (f(j))"
        print "------------------------------------"


        if stage==0:
            F[stage]=[]
            i=0
            for j in range(i,len(words)+1):
                print "i=",i,"j=",j,"\t\t\t",cost(i,j)
                F[stage].append([cost(i,j),0])
        elif stage==(n-1):
            F[stage]=[[float('inf'),0] for i in range(len(words)+1)]
            for i in range(len(words)+1):
                    j=len(words)
                    if F[stage-1][i][0]+cost(i,j)<F[stage][j][0]: #calculating min cost (cf f formula)
                        F[stage][j][0]=F[stage-1][i][0]+cost(i,j)
                        F[stage][j][1]=i
                        print "i=",i,"j=",j,"\t\t\t",F[stage][j][0]            
        else:
            F[stage]=[[float('inf'),0] for i in range(len(words)+1)]
            for i in range(len(words)+1):
                for j in range(i,len(words)+1):
                    if F[stage-1][i][0]+cost(i,j)<F[stage][j][0]:
                        F[stage][j][0]=F[stage-1][i][0]+cost(i,j)
                        F[stage][j][1]=i
                        print "i=",i,"j=",j,"\t\t\t",F[stage][j][0]

    print 'reversing list'
    print "------------------------------------"
    listWords=[]
    a=len(words)
    for k in xrange(n-1,0,-1):#reverse loop from n-1 to 1
        listWords.append(' '.join(words[F[k][a][1]:a]))
        a=F[k][a][1]
    listWords.append(' '.join(words[0:a]))
    listWords.reverse()

    for line in listWords:
        print line, '\t\t',len(line)

    return listWords

【讨论】:

  • 你不需要动态编程。您只是想获得固定行数的最小宽度。贪婪与二分搜索足以解决这个问题。看看我的答案或 btilly 的答案。
  • 你的答案比我的复杂度低(我的程序在 O(n2) 中),你说得对,动态编程并不是唯一的方法。我试图解释 peteT 在他的编辑中放入的代码,因为它似乎是一个问题。感谢您的建议,我会更仔细地查看 btilly 和您的答案。
  • 它不适用于:``` >>> minimum_width(['a', 'b', 'c', 'dad', 'e', 'f'], 3) ['a b', 'c dad', 'e f'] >>> minimum_width(['a', 'b', 'cad', 'd', 'e', 'f'], 3) [ 'a b', 'cad', 'd e f'] # 9+9 = 18 ``` 在后一种情况下,我们将包装为: ``` ['a b', 'cad d', 'e f'] # 4+4 = 8 ```
【解决方案2】:

这是从Algorithm to divide text into 3 evenly-sized groups 转换为 C# 的公认解决方案:

static List<string> Minragged(string text, int n = 3)
{
    var words = text.Split();

    var cumwordwidth = new List<int>();
    cumwordwidth.Add(0);

    foreach (var word in words)
        cumwordwidth.Add(cumwordwidth[cumwordwidth.Count - 1] + word.Length);

    var totalwidth = cumwordwidth[cumwordwidth.Count - 1] + words.Length - 1;

    var linewidth = (double)(totalwidth - (n - 1)) / n;

    var cost = new Func<int, int, double>((i, j) =>
    {
        var actuallinewidth = Math.Max(j - i - 1, 0) + (cumwordwidth[j] - cumwordwidth[i]);
        return (linewidth - actuallinewidth) * (linewidth - actuallinewidth);
    });

    var best = new List<List<Tuple<double, int>>>();

    var tmp = new List<Tuple<double, int>>();
    best.Add(tmp);
    tmp.Add(new Tuple<double, int>(0.0f, -1));
    foreach (var word in words)
        tmp.Add(new Tuple<double, int>(double.MaxValue, -1));

    for (int l = 1; l < n + 1; ++l)
    {
        tmp = new List<Tuple<double, int>>();
        best.Add(tmp);
        for (int j = 0; j < words.Length + 1; ++j)
        {
            var min = new Tuple<double, int>(best[l - 1][0].Item1 + cost(0, j), 0);
            for (int k = 0; k < j + 1; ++k)
            {
                var loc = best[l - 1][k].Item1 + cost(k, j);
                if (loc < min.Item1 || (loc == min.Item1 && k < min.Item2))
                    min = new Tuple<double, int>(loc, k);
            }
            tmp.Add(min);
        }
    }

    var lines = new List<string>();
    var b = words.Length;

    for (int l = n; l > 0; --l)
    {
        var a = best[l][b].Item2;
        lines.Add(string.Join(" ", words, a, b - a));
        b = a;
    }

    lines.Reverse();
    return lines;
}

【讨论】:

  • 感谢阅读 python 对我来说很难,因为我还不熟悉它。我可能会尝试学习它。我将在周末开放赏金,以防万一其他人想改进答案列表,但我可能会奖励给你。
  • 如果您不在 .NET 4 项目中,您可以将元组转换为 KeyValuePair
【解决方案3】:

http://www.perlmonks.org/?node_id=180276 曾讨论过这个确切的问题(尽管措辞不同)。

最后,最好的解决方案是对所有可能的宽度进行二进制搜索,以找到不超过所需列数的最小宽度。如果有n 项目并且平均宽度是m,那么您将需要O(log(n) + log(m)) 传递来找到正确的宽度,每个都需要O(n) 时间,对于O(n * (log(n) + log(m)))。这可能已经足够快了,不需要再聪明了。

如果你想聪明一点,你可以创建一个字数数组和单词的累积长度。然后在这个数据结构上使用二进制搜索来找出换行符在哪里。创建这个数据结构是O(n),它使所有的pass都找出正确的宽度是O(log(n) * (log(n) + log(m))),对于合理的单词长度,这由你的第一个O(n) pass控制。

如果单词的宽度可以是浮点数,则您需要对二分搜索做一些更聪明的事情,但您不太可能需要这种特定的优化。

【讨论】:

    【解决方案4】:

    btilly 在这里有正确的答案,但只是为了好玩,我决定用 python 编写一个解决方案:

    def wrap_min_width(words, n):
        r, l = [], ""
        for w in words:
            if len(w) + len(l) > n:
                r, l = r + [l], ""
            l += (" " if len(l) > 0 else "") + w
        return r + [l]  
    
    def min_lines(phrase, lines):
        words = phrase.split(" ")
        hi, lo = sum([ len(w) for w in words ]), min([len(w) for w in words])
        while lo < hi:
            mid = lo + (hi-lo)/2
            v = wrap_min_width(words, mid)
            if len(v) > lines:
                lo = mid + 1
            elif len(v) <= lines:
                hi = mid
        return lo, "\n".join(wrap_min_width(words, lo))
    

    现在这可能仍然不是您想要的,因为如果可以使用相同的线宽将单词换行少于 n 行,则它会返回最少的行数编码。 (当然你总是可以添加额外的空行,但这有点傻。)如果我在你的测试用例上运行它,我会得到:

    案例:“我想被包裹成三行”,3行

    结果:1​​4 个字符/行

    我愿意

    被包裹起来

    三行

    【讨论】:

      【解决方案5】:

      我刚刚想到了一个办法:
      您可以编写一个接受两个参数的函数 1. 字符串 2. 行数

      获取字符串的长度(如果使用 C#,则为 String.length)。 将长度除以行数(假设结果为 n)

      现在开始一个循环并访问字符串的每个字符(使用 string[i]) 在字符数组中每第 n 次出现后插入一个 '\n\r'。

      在循环中维护一个临时字符串数组,如果有一个空白字符(维护每个单词),该数组将为空。
      如果第 n 次出现并且临时字符串不为空,则在该临时字符串之后插入 '\n\r'。

      【讨论】:

        【解决方案6】:

        我假设您正在尝试使用 n 个断点来最小化字符串的最大宽度。这可以使用动态编程或带有记忆的递归在 O(words(str)*n) 时间和空间内完成。

        在单词被拆分为单词的情况下,重复出现如下所示

        def wordwrap(remaining_words, n):
            if n > 0 and len(remaining_words)==0:
                return INFINITY  #we havent chopped enough lines
        
            if n == 0:
                return len(remaining_words.join(' ')) # rest of the string
        
            best = INFINITY
            for i in range remaining_words:
                # split here 
                best = min( max(wordwrap( remaining_words[i+1:], n-1),remaining_words[:i].join(' ')), best  )  
        
            return best
        

        【讨论】:

          【解决方案7】:

          我将 C# 接受的答案转换为 JavaScript,用于我正在处理的事情。将其发布在此处可能会为某人节省几分钟的时间。

          function WrapTextWithLimit(text, n) {
              var words = text.toString().split(' ');
              var cumwordwidth = [0];
              words.forEach(function(word) {
                  cumwordwidth.push(cumwordwidth[cumwordwidth.length - 1] + word.length);
              });
              var totalwidth = cumwordwidth[cumwordwidth.length - 1] + words.length - 1;
              var linewidth = (totalwidth - (n - 1.0)) / n;
              var cost = function(i, j) {
                  var actuallinewidth = Math.max(j - i - 1, 0) + (cumwordwidth[j] - cumwordwidth[i]);
                  return (linewidth - actuallinewidth) * (linewidth - actuallinewidth);
              };
              var best = [];
              var tmp = [];
              best.push(tmp);
              tmp.push([0.0, -1]);
              words.forEach(function(word) {
                  tmp.push([Number.MAX_VALUE, -1]);
              });
              for (var l = 1; l < n + 1; ++l)
              {
                  tmp = [];
                  best.push(tmp);
                  for (var j = 0; j < words.length + 1; ++j)
                  {
                      var min = [best[l - 1][0][0] + cost(0, j), 0];
                      for (var k = 0; k < j + 1; ++k)
                      {
                          var loc = best[l - 1][k][0] + cost(k, j);
                          if (loc < min[0] || (loc === min[0] && k < min[1])) {
                              min = [loc, k];
                          }
                      }
                      tmp.push(min);
                  }
              }
              var lines = [];
              var b = words.length;
              for (var p = n; p > 0; --p) {
                  var a = best[p][b][1];
                  lines.push(words.slice(a, b).join(' '));
                  b = a;
              }
              lines.reverse();
              return lines;
          }
          

          【讨论】:

            【解决方案8】:

            此解决方案改进了 Mikola 的解决方案。

            这样更好,因为

            1. 它不使用字符串。您不需要使用字符串并将它们连接起来。你只需要它们长度的数组。因此,由于它更快,您也可以将此方法与任何类型的“元素”一起使用 - 您只需要宽度。
            2. wrap_min_width 函数中有一些不必要的处理。即使超出了故障点,它仍然继续运行。此外,它只是不必要地构建字符串。
            3. 添加了“分隔符宽度”作为可调参数。
            4. 它会计算最小宽度 - 这正是您想要的。
            5. 修复了一些错误。

            这是用 Javascript 编写的:

             // For testing calcMinWidth
            
            var formatString = function (str, nLines) {
            
                var words = str.split(" ");
                var elWidths = words.map(function (s, i) {
                    return s.length;
                });
            
                var width = calcMinWidth(elWidths, 1, nLines, 0.1);
            
                var format = function (width)
                {
                    var lines = [];
                    var curLine = null;
                    var curLineLength = 0;
            
                    for (var i = 0; i < words.length; ++i) {
                        var word = words[i];
                        var elWidth = elWidths[i];
            
                        if (curLineLength + elWidth > width)
                        {
                            lines.push(curLine.join(" "));
                            curLine = [word];
                            curLineLength = elWidth;
                            continue;
                        }
            
                        if (i === 0)
                            curLine = [word];
                        else
                        {
                            curLineLength += 1;
                            curLine.push(word);
                        }
            
                        curLineLength += elWidth;
                    }
            
                    if (curLine !== null)
                        lines.push(curLine.join(" "));
            
                    return lines.join("\n");
                };
            
                return format(width);
            };
            
            var calcMinWidth = function (elWidths, separatorWidth, lines, tolerance)
            {
                var testFit = function (width)
                {
                    var nCurLine = 1;
                    var curLineLength = 0;
            
                    for (var i = 0; i < elWidths.length; ++i) {
                        var elWidth = elWidths[i];
            
                        if (curLineLength + elWidth > width)
                        {
                            if (elWidth > width)
                                return false;
            
                            if (++nCurLine > lines)
                                return false;
            
                            curLineLength = elWidth;
                            continue;
                        }
            
                        if (i > 0)
                            curLineLength += separatorWidth;
            
                        curLineLength += elWidth;
                    }
            
                    return true;
                };
            
            
                var hi = 0;
                var lo = null;
            
                for (var i = 0; i < elWidths.length; ++i) {
                    var elWidth = elWidths[i];
            
                    if (i > 0)
                        hi += separatorWidth;
            
                    hi += elWidth;
            
                    if (lo === null || elWidth > lo)
                        lo = elWidth;
                }
            
                if (lo === null)
                    lo = 0;
            
                while (hi - lo > tolerance)
                {
                    var guess = (hi + lo) / 2;
            
                    if (testFit(guess))
                        hi = guess;
                    else
                        lo = guess;
                }
            
                return hi;
            };
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-09-14
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-12-29
              相关资源
              最近更新 更多