Word换行到X行而不是最大宽度(最小粗糙度)

时间:2011-06-21 13:28:03

标签: algorithm word-wrap

有没有人知道将输入字符串换行到指定行数而不是设置宽度的好算法。基本上是为了达到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。

8 个答案:

答案 0 :(得分:5)

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


<强>符号:

让你的文字文件命名=“word1 word2 .... wordp”

n =所需的行数

线宽= LEN(文件)/ N


成本函数:

首先你需要定义一个成本函数,在同一行中有单词[i]到单词[j],你可以采用与维基百科上的那个相同的单词,例如p = 2:

cost function

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

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

enter image description here


解决问题:

您可以使用动态编程解决此问题。 我从您给出的链接中获取了代码,并将其更改为a,以便您了解该程序正在使用的内容。

  1. 在第k阶段,你可以在第k行添加单词。
  2. 然后你看看最优的成本 在第k行有字i到j。
  3. 一旦你从第1行走到n, 你解决了最小的成本 最后一步,你有最佳状态 结果:
  4. 以下是代码的结果:

    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个单词 的stage2)
    • 然后在第二行中的单词2到5(参见 的stage1)
    • 然后在第一行中的字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
    

答案 1 :(得分:4)

http://www.perlmonks.org/?node_id=180276讨论了这个确切的问题(虽然它以不同的方式表达)。

最后,最好的解决方案是对所有可能的宽度进行二进制搜索,以找到最小宽度,最小宽度不超过所需的列数。如果有n个项目且平均宽度为m,那么您需要O(log(n) + log(m))次传递以找到正确的宽度,每个宽度花费O(n)次{ {1}}。这可能足够快,不再需要聪明。

如果您希望聪明,可以创建一个单词计数数组和单词的累计长度。然后在此数据结构上使用二进制搜索来确定换行符的位置。创建这个数据结构是O(n * (log(n) + log(m))),它会使所有传递得出正确的宽度为O(n),其中合理长度的单词由您的第一个O(log(n) * (log(n) + log(m)))传递控制。

如果单词的宽度可以是浮点数,那么你需要用二进制搜索做一些更聪明的事情,但你不太可能需要那种特殊的优化。

答案 2 :(得分:4)

以下是从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;
}

答案 3 :(得分:3)

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个字符/行

     
    

我想

         

被包裹进

         

三行

  

答案 4 :(得分:0)

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

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

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

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

答案 5 :(得分:0)

我假设你试图用n次中断最小化字符串的最大宽度。这可以使用动态编程或带有memoziation的递归在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

答案 6 :(得分:0)

我将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;
}

答案 7 :(得分:0)

此解决方案在Mikola的基础上有所改进。

最好是因为

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

这是用Java脚本编写的:

 // 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;
};
相关问题