如何有效地找到一定宽度的字符串的理想列数?

时间:2014-06-25 14:43:06

标签: algorithm language-agnostic formatting multiple-columns

我有 n 不同长度的字符串 s 1 ,s 2 ,...,s n 我希望在 c 列的终端上显示。终端的宽度为 m 个字符。每列 i 具有一定的宽度 w i ,它等于该列中最长条目的宽度。在每对列之间有一定量的空间 s 。包括空间之间的所有列的总宽度不能大于终端的宽度( w 1 + w 2 + ... + w c + c - 1 ·s≤m)。每列应包含⌈ n / c ⌉字符串,除非 n 不能由 c 均匀分割,在这种情况下,最后几列应为一个条目可以缩短,或者只有最后一列可以更短,具体取决于字符串是向上还是向下排列。

是否有效(例如 O n·w w = max( w 1 < / sub>,w 2 ,...,w n ))算法,以确定我可以容纳的最大列数 c < / em>列,如果......

  • 字符串排列在

    之间
    string1 string2  string3 string4
    string5 string6  string7 string8
    string9 string10
    
  • 字符串排列

    string1 string4 string7 string10
    string2 string5 string8
    string3 string6 string9
    

后来的发现

我发现 s 并不重要。问题的每个实例 s&gt;通过 s 字符扩展每个字符串并通过 s <扩展终端的宽度,可以将0 转换为 s = 0 的实例/ em>字符用于补偿屏幕末尾的额外 s 字符。

4 个答案:

答案 0 :(得分:4)

不幸的是,我认为你可以拥有的最快算法是O(n ^ 2)。这是因为您可以确定列表的单次传递中是否可以对c列进行配置,但是您无法知道更改c的程度,因此基本上您只需尝试不同的值即可它。最多你的算法会这样做n次。

这是我将如何做的伪代码

for c = n.size, c > 0, c --
  remainingDist = m - c*s
  for i = 1, i <= c, i ++ 
    columnWidth = 0
    for j = i, j <= n.size; j += c
      columnWidth = max(n[j], columnWidth)
    end
    remainingDist -= columnWidth
  end
  if remainingDist >= 0
    success
    columns = c
    break
  end
end

你可以通过首先计算物品的平均大小来跳到循环的中途,然后找出一个理想的&#34;从那里开始的列数。

答案 1 :(得分:3)

我不确定如何在代码中详细说明这个,但是根据大小排序的字符串的直方图,我们可以设置列数的理论上限,以进一步细化确切的方法,如wckd&#39; s。由于列大小由其最长元素决定,我们不得不尽可能均匀地划分列,只要到目前为止最大字符串的数量是总数的一小部分,我们可以继续拆分列短弦。例如,

size frequency
10   5
 6   10
 3   11

m = 30, s = 1

start: 30 - (10 + 1) = 19
implies 13 + 13:

10 (x5)  6 (x2) 
 6 (x8)  3 (x11)

but the portion of 6 and 3 is large enough to split the second column:

19 - (6 + 1) = 12
implies 9 + 9 + 8:

10 (x5) 6 (x6) 3 (x8)
6  (x4) 3 (x3)

but splitting again will still fit:
12 - (6 + 1) = 5
implies 7 + 7 + 6 + 6:

10 (x5) 6 (x7) 6 (x1) 3 (x6)
6  (x2)        3 (x5)

理论上我们最多得到4列(显然,在实际结果中不允许对字符串进行排序),这可以通过wckd方法减少。

根据数据的不同,我想知道这种优化是否有时会有用。构建直方图应该花费O(n + k * log k)时间和O(k)空格,其中k是字符串大小的数量(您已经使用w < 1000, m < 10000限制)。我建议的操作实际上独立于n,它仅取决于msk的分布;由于k已排序,我们只需要一次分割/计算列。

答案 2 :(得分:2)

我查看了第一个问题(水平填充),并假设间隙大小(s)为零,就像您在编辑中建议的那样。

首先:我知道赏金已经结束,而且我没有证据表明算法的效果优于O(n²)

但是,我确实有一些想法可能仍然有用。

我提出的算法如下:

c时间内获取O(n)的上限(稍后我会介绍)

如果c为0或1,或者所有字符串都在一行上,那么c就是答案。停止。

使用ss[] s[]O(w+n)),使用pigeon hole sortw = max(s[]), w <= m上以降序宽度创建索引ss[]width的一个元素有两个属性:seqNos[](原始序列号,如c中所示)。

然后,按递减顺序循环宽度,直到m - 列配置中的每列都有宽度。 如果这些宽度的总和仍然不大于c,那么 knownColumnWidths = new Set() of column numbers sumWidth = 0 for i from 0 to n-1: colNo = ss[i].seqNo modulo c if not colNo in knownColumnWidths: sumWidth += ss[i].width if sumWidth > m: // c is not a solution, exit for-loop break add colNo to knownColumnWidths if knownColumnWidths has c values: // solution found return c as solution. Stop 就是一个解决方案。更正式地说:

c

如果c = c - 1被拒绝作为解决方案,请使用O(n²)重复以前的代码。

算法的最后一部分似乎是n - c + 1。但是,如果for循环具有最差的性能(即c/2次迭代),那么接下来几次(c)它运行,它将接近最佳性能(即接近{{ 1}}迭代)。但最终它仍然看起来像O(n²)

为了获得c的良好上限(参见上面的第一步),我建议:

首先在终端的第一行填写尽可能多的字符串而不超过限制m,并将其作为c的初始上限。更正式地说:

sumWidth = 0
c = 0
while c < n and sumWidth + s[c] <= m:
    sumWidth += s[c]
    c++

这显然是O(n)

这可以进一步改进如下:

c宽度的总和,但进一步开始一个字符串,并检查这是否仍然不大于m。继续这种转变。超过m后,减少c,直到c宽度的总和再次为OK,并以c个连续宽度的总和继续移位。

更正式地说,c从上面找到的上限开始:

for i from c to n - 1:
    if s[i] > m:
        c = 0. Stop // should not happen: all widths should be <= m
    sumWidth += s[i] - s[i - c]
    while sumWidth > m:
        // c is not a solution. Improve upper limit: 
        c--
        sumWidth -= s[i - c]

这意味着在一次扫描中,您可能会对c进行一些改进。当然,在最坏的情况下,它根本没有任何改善。

我的算法结束了。我估计它在随机输入上表现良好,但仍然看起来像O(n²)

但是我有一些观察,我在上面的算法中没有使用过:

  1. 如果找到某个c的列宽,但总宽度大于m,那么此结果仍可用于c'=c/2的情况。然后没有必要通过所有字符串宽度。只需sum(max(s[i], s[i+c']) i in 0..c'-1即可。同样的原则适用于c的其他除数。
  2. 我没有使用它,因为如果你必须从c一直向下c/2而没有找到解决方案,那么你已经在算法上花了O(n²)。但也许它可以用于另一种算法的目的......

    1. 当不允许零长度字符串时,可以将算法设为O(m.n),因为可能的解决方案的数量(c的值)被限制为m并且要确定其中一个是否是解决方案,只需要扫描所有宽度。
    2. 我允许使用零长度字符串。

      1. 最初我正在研究c的二分搜索,将其除以2,然后选择其余的一半。但是这种方法无法使用,因为即使发现某个c不是解决方案,也不排除存在c' > c解决方案。
      2. 希望这个答案中有一些你觉得有用的东西。

答案 3 :(得分:2)

解决此问题的一个明显方法是遍历所有字符串长度 以某种预定顺序,更新每个字符串所在列的宽度 属于,当列宽的总和超过终端宽度时停止。然后重复一遍 此过程用于递减列数(或用于递增的行数) “安排下来”案例)直到成功。这个预定的三种可能的选择 顺序:

  1. by row(考虑第一行中的所有字符串,然后是第2,3行......)
  2. by column(考虑第一列中的所有字符串,然后是第2,3列,......)
  3. 按值(按长度排序字符串,然后按排序顺序迭代它们,从最长的开始)。
  4. 这三种方法可以“排列”子问题。但是为了“安排下来” 子问题他们都有最坏的案例时间复杂度O(n 2 )。前两个 方法即使对于随机数据也显示二次复杂度。 “按价值”方法很漂亮 适用于随机数据,但很容易找到最坏的情况:只需分配短 字符串到列表的前半部分和长字符串 - 到下半部分。

    在这里,我描述了一种“安排”案例的算法,该算法没有这些 缺点。

    不是分别检查每个字符串长度,而是确定每个字符串的宽度 O(1)时间列在范围最大查询(RMQ)的帮助下。第一个宽度 column只是范围中的最大值(0 .. num_of_rows),下一个的宽度是 范围内的最大值(num_of_rows .. 2 * num_of_rows)等

    要在O(1)时间内回答每个查询,我们需要准备一个最大数组 范围内的值(0 ... 2 k ),(1 ... 1 + 2 k ),...,其中k是 最大整数,使得2 k 不大于当前行数。每个范围最大查询计算为此数组中最多两个条目。 当行数开始太大时,我们应该将此查询数组从k更新为k + 1(每个此类更新都需要O(n)范围查询)。

    这是C ++ 14实现:

    template<class PP>
    uint32_t arrangeDownRMQ(Vec& input)
    {
        auto rows = getMinRows(input);
        auto ranges = PP{}(input, rows);
    
        while (rows <= input.size())
        {
            if (rows >= ranges * 2)
            { // lazily update RMQ data
                transform(begin(input) + ranges, end(input), begin(input),
                          begin(input), maximum
                );
    
                ranges *= 2;
            }
            else
            { // use RMQ to get widths of columns and check if all columns fit
                uint32_t sum = 0;
    
                for (Index i = 0; sum <= kPageW && i < input.size(); i += rows)
                {
                    ++g_complexity;
                    auto j = i + rows - ranges;
    
                    if (j < input.size())
                        sum += max(input[i], input[j]);
                    else
                        sum += input[i];
                }
    
                if (sum <= kPageW)
                {
                    return uint32_t(ceilDiv(input.size(), rows));
                }
    
                ++rows;
            }
        }
    
        return 0;
    }
    

    这里PP是可选的,对于简单的情况,这个函数对象什么都不做 返回1.

    要确定此算法的最坏情况时间复杂度,请注意外部循环开始 使用rows = n * v / m(其中v是平均字符串长度,m是页面宽度)和 停止最多rows = n * w / m(其中w是最大字符串长度)。 “查询”循环中的迭代次数不大于列数或 n / rows。将这些迭代一起添加O(n * (ln(n*w/m) - ln(n*v/m)))O(n * log(w/v))。这意味着具有小常数因子的线性时间。

    我们应该在这里添加时间来执行所有更新操作,即获得O(n log n) 整个算法的复杂性:O(n * log n)。

    如果在完成某些查询操作之前我们不执行更新操作,则需要时间 对于更新操作以及算法复杂度降低到O(n * log(w/v))。 为了实现这一点,我们需要一些用最大值填充RMQ数组的算法 给定长度的子阵列。我尝试了两种可能的方法 algorithm with pair of stacks 稍快一点。这是C ++ 14实现(输入数组用于实现两个堆栈以降低内存需求并简化代码):

    template<typename I, typename Op>
    auto transform_partial_sum(I lbegin, I lend, I rbegin, I out, Op op)
    { // maximum of the element in first enterval and prefix of second interval
        auto sum = typename I::value_type{};
    
        for (; lbegin != lend; ++lbegin, ++rbegin, ++out)
        {
            sum = op(sum, *rbegin);
            *lbegin = op(*lbegin, sum);
        }
    
        return sum;
    }
    
    template<typename I>
    void reverse_max(I b, I e)
    { // for each element: maximum of the suffix starting from this element
        partial_sum(make_reverse_iterator(e),
                    make_reverse_iterator(b),
                    make_reverse_iterator(e),
                    maximum);
    }
    
    struct PreprocRMQ
    {
        Index operator () (Vec& input, Index rows)
        {
            if (rows < 4)
            { // no preprocessing needed
                return 1;
            }
    
            Index ranges = 1;
            auto b = begin(input);
    
            while (rows >>= 1)
            {
                ranges <<= 1;
            }
    
            for (; b + 2 * ranges <= end(input); b += ranges)
            {
                reverse_max(b, b + ranges);
                transform_partial_sum(b, b + ranges, b + ranges, b, maximum);
            }
    
            // preprocess inconvenient tail part of the array
            reverse_max(b, b + ranges);
            const auto d = end(input) - b - ranges;
            const auto z = transform_partial_sum(b, b + d, b + ranges, b, maximum);
            transform(b + d, b + ranges, b + d, [&](Data x){return max(x, z);});
            reverse_max(b + ranges, end(input));
            return ranges;
        }
    };
    

    在实践中,看到一个简短的单词比一个长单词更有可能。 在英文文本中,较短的单词数量较长,文本表示较短 自然数也占上风。所以我选择(略微修改)字符串的几何分布 长度来评估各种算法。 Here是整个基准程序(in C ++ 14 for gcc)。同一程序的旧版本包含一些过时的测试和一些算法的不同实现:TL;DR。以下是结果:

    time

    对于“安排穿越”案件:

    • “by column”方法是最慢的
    • “按值”方法适用于n = 1000..500'000'000
    • “by row”方法适用于小尺寸和超大尺寸

    对于“安排下来”案件:

    • “按值”方法是可行的,但比其他选择更慢(一种可能使其更快的方法是通过乘法实现除法),它也使用更多内存并具有二次最坏情况时间
    • 简单的RMQ方法更好
    • RMQ预处理效果最佳:它具有线性时间复杂度,并且其内存带宽要求非常低。

    看到每种算法所需的迭代次数可能也很有趣。 这里排除了所有可预测的部分(排序,求和,预处理和RMQ) 更新):

    iter