我有 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 字符。
答案 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
,它仅取决于m
,s
和k
的分布;由于k
已排序,我们只需要一次分割/计算列。
答案 2 :(得分:2)
我查看了第一个问题(水平填充),并假设间隙大小(s
)为零,就像您在编辑中建议的那样。
首先:我知道赏金已经结束,而且我没有证据表明算法的效果优于O(n²)
。
但是,我确实有一些想法可能仍然有用。
我提出的算法如下:
在c
时间内获取O(n)
的上限(稍后我会介绍)
如果c
为0或1,或者所有字符串都在一行上,那么c
就是答案。停止。
使用ss[]
s[]
(O(w+n)
),使用pigeon hole sort在w = max(s[]), w <= m
上以降序宽度创建索引ss[]
。 width
的一个元素有两个属性:seqNo
和s[]
(原始序列号,如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²)
。
但是我有一些观察,我在上面的算法中没有使用过:
c
的列宽,但总宽度大于m
,那么此结果仍可用于c'=c/2
的情况。然后没有必要通过所有字符串宽度。只需sum(max(s[i], s[i+c'])
i in 0..c'-1
即可。同样的原则适用于c
的其他除数。我没有使用它,因为如果你必须从c
一直向下c/2
而没有找到解决方案,那么你已经在算法上花了O(n²)
。但也许它可以用于另一种算法的目的......
O(m.n)
,因为可能的解决方案的数量(c
的值)被限制为m
并且要确定其中一个是否是解决方案,只需要扫描所有宽度。我允许使用零长度字符串。
c
的二分搜索,将其除以2,然后选择其余的一半。但是这种方法无法使用,因为即使发现某个c
不是解决方案,也不排除存在c' > c
解决方案。希望这个答案中有一些你觉得有用的东西。
答案 3 :(得分:2)
解决此问题的一个明显方法是遍历所有字符串长度 以某种预定顺序,更新每个字符串所在列的宽度 属于,当列宽的总和超过终端宽度时停止。然后重复一遍 此过程用于递减列数(或用于递增的行数) “安排下来”案例)直到成功。这个预定的三种可能的选择 顺序:
这三种方法可以“排列”子问题。但是为了“安排下来” 子问题他们都有最坏的案例时间复杂度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。以下是结果:
对于“安排穿越”案件:
对于“安排下来”案件:
看到每种算法所需的迭代次数可能也很有趣。 这里排除了所有可预测的部分(排序,求和,预处理和RMQ) 更新):