N=3, L=2, R=1
,只有一种安排{2, 1, 3}
,而对于N=3, L=2, R=2
,有两种方法{1, 3, 2}
和{2, 3, 1}
。我们应该如何通过编程解决这个问题?有什么有效的方法吗?
N=3, L=2, R=1
,只有一种安排{2, 1, 3}
,而对于N=3, L=2, R=2
,有两种方法{1, 3, 2}
和{2, 3, 1}
。b(N,L,R)
为方案数量,f(N,L)
为在左侧可见的N
块的排列数量。首先考虑f
,因为它更容易。
方法一
让我们获取初始条件,然后进行递归。如果所有块都要可见,则它们必须按递增顺序排列,因此:f(N, N) = 1
f(N, M) = 0 if N < M
如果只有一个块应该是可见的,那么请先放置最大的块,然后其他块可以以任何顺序跟随。
f(N,1) = (N-1)!
最后,考虑递归时,想想最高的块的位置,假设N
在从左边开始的第k
个位置。然后选择在它前面的块有(N-1 choose k-1)
种方法,在这些块中排列,以便左边恰好可见L-1
个块,并将N-k
块按任意顺序放在N
的后面:
f(N, L) = sum_{1<=k<=N} (N-1 choose k-1) * f(k-1, L-1) * (N-k)!
事实上,由于在 x<L
的情况下 f(x-1,L-1) = 0
,因此我们可以从 L
开始而不是从 1
开始进行 k
:
f(N, L) = sum_{L<=k<=N} (N-1 choose k-1) * f(k-1, L-1) * (N-k)!
好的,现在我们已经理解了简单部分,让我们使用 f
来解决更难的部分 b
。同样,基于最高方块的位置使用递归,设 N
位于左侧的位置 k
。和之前一样,在 N-1 choose k-1
种方式中选择它之前的方块,但是现在需要分别考虑该块的每一侧。对于 N
左侧的 k-1
个块,请确保其中恰好有 L-1
个可见。对于 N
右侧的 N-k
个块,请确保其中有 R-1
个可见,并且反转从 f
得到的顺序。因此答案为:
b(N,L,R) = sum_{1<=k<=N} (N-1 choose k-1) * f(k-1, L-1) * f(N-k, R-1)
其中f
已经完全处理好了。同样地,很多项都会为零,因此我们只需要选择k
使得k-1>=L-1
和N-k>=R-1
,就可以得到
b(N,L,R) = sum_{L <= k <= N-R+1} (N-1 choose k-1) * f(k-1, L-1) * f(N-k, R-1)
方法2
我再次思考这个问题,找到了一种更好的方法,避免了求和。
如果你反向思考问题,想着加上最小块而不是最大块,那么 f
的递归公式就会简单得多。在这种情况下,使用相同的初始条件,递归公式为:
f(N,L) = f(N-1,L-1) + (N-1) * f(N-1,L)
其中第一个项f(N-1,L-1)
表示将最小的块放在最左侧,从而增加一个可见块(因此L
减少为L-1
),第二个项(N-1)*f(N-1,L)
表示将最小的块放在N-1
个非前置位置之一,此时它不可见(因此L
保持不变)。
这个递归的优点是始终减少N
,但使得有些公式更难以看出来,例如f(N,N-1)=(N choose 2)
。虽然可以从前一个公式中很容易地证明这个公式,但我不确定如何从这个更简单的递推关系中漂亮地推导它。
现在,要回到原问题并解决b
,我们也可以采用不同的方法。不要考虑之前的求和,而是将可见块视为分成数据包,因此如果从左边可见,则其数据包包括所有块到下一个从左边可见的块右侧和前面,如果从右边可见,则其数据包包含所有块到下一个从右边可见的块左侧。对除最高块外的所有块都这样做。这样一共有L+R
个数据包。给定数据包,您可以通过反转块的顺序将一个数据包从左侧移动到右侧。因此,一般情况下的b(N,L,R)
实际上缩减为解决案例b(N,L,1)=f(N,L)
,然后选择将哪些数据包放在左侧和哪些放在右侧。因此我们有
b(N,L,R) = (L+R choose L) * f(N,L+R)
再次,这种重新表述相对于先前的版本有一些优点。将后两个公式结合起来,更容易看出整个问题的复杂性。然而,我仍然更喜欢第一种方法来构建解决方案,尽管可能会有人持不同意见。总体而言,这表明了解决问题的好方法不止一种。
Stirling数是什么?
正如Jason指出的那样,f(N, L)
数恰好是(无符号的)第一类斯特林数。可以立即从每个递归公式中看到这一点。然而,直接看到它总是很好的,下面就是详细说明。
第一类斯特林数(unsigned Stirling numbers of the First Kind)用S(N, L)
表示,计算将N
排成L
个环的置换数量。通过在循环符号中写出置换,通过将循环以循环中最大的数字开头并按照循环中的第一个数字按升序排序来编写规范形式的置换。例如,置换
(2 6) (5 1 4) (3 7)
按规范形式编写的内容如下:
(5 1 4) (6 2) (7 3)
b(N,L,1) = f(N-1,L-1)
。因为最高的块应该固定在最后一个位置(从左右两侧都可见),所以从右侧看只有一个可见的块。 - Priyank Bhatnagarb(N,L,1)=f(N-1,L-1)
。最高的块需要特别注意。而且我认为b(N,L,R)=(L+R-2 choose L-1)*f(N-1,L+R-2)
似乎更像是正确的答案,而不是b(N,L,R)=(L+R choose L)*f(N,L+R)
,这会导致b(3,2,1)
的错误答案“3”。无论如何,感谢优秀的解决方案。 - ZZY好的,针对小 N 的经验性解决方案如下:
blocks.py:
import itertools
from collections import defaultdict
def countPermutation(p):
n = 0
max = 0
for block in p:
if block > max:
n += 1
max = block
return n
def countBlocks(n):
count = defaultdict(int)
for p in itertools.permutations(range(1,n+1)):
fwd = countPermutation(p)
rev = countPermutation(reversed(p))
count[(fwd,rev)] += 1
return count
def printCount(count, n, places):
for i in range(1,n+1):
for j in range(1,n+1):
c = count[(i,j)]
if c > 0:
print "%*d" % (places, count[(i,j)]),
else:
print " " * places ,
print
def countAndPrint(nmax, places):
for n in range(1,nmax+1):
printCount(countBlocks(n), n, places)
print
和样本输出:
blocks.countAndPrint(10)
1
1
1
1 1
1 2
1
2 3 1
2 6 3
3 3
1
6 11 6 1
6 22 18 4
11 18 6
6 4
1
24 50 35 10 1
24 100 105 40 5
50 105 60 10
35 40 10
10 5
1
120 274 225 85 15 1
120 548 675 340 75 6
274 675 510 150 15
225 340 150 20
85 75 15
15 6
1
720 1764 1624 735 175 21 1
720 3528 4872 2940 875 126 7
1764 4872 4410 1750 315 21
1624 2940 1750 420 35
735 875 315 35
175 126 21
21 7
1
5040 13068 13132 6769 1960 322 28 1
5040 26136 39396 27076 9800 1932 196 8
13068 39396 40614 19600 4830 588 28
13132 27076 19600 6440 980 56
6769 9800 4830 980 70
1960 1932 588 56
322 196 28
28 8
1
40320 109584 118124 67284 22449 4536 546 36 1
40320 219168 354372 269136 112245 27216 3822 288 9
109584 354372 403704 224490 68040 11466 1008 36
118124 269136 224490 90720 19110 2016 84
67284 112245 68040 19110 2520 126
22449 27216 11466 2016 126
4536 3822 1008 84
546 288 36
36 9
1
从问题陈述中可以看到一些明显的(或大多数明显的)事情:
从经验输出中可以看出:
具有N个块的最左侧列或最顶部行(其中L = 1或R = 1)是具有N-1个块的行/列之和:即在@PengOne的符号中,
b(N,1,R)= sum(b(N-1,k,R-1)for k = 1 to N-R+1
每个对角线都是Pascal三角形的一行,乘以该对角线的常数因子K-我不能证明这一点,但我确信有人可以-即:
b(N,L,R)= K *(L+R-2选择L-1)
其中K = b(N,1,L+R-1)
通过Online Encyclopedia of Integer Sequences的快速检查,发现b(N,1,R)似乎是OEIS sequence A094638:
A094638 按行读取的三角形:T(n,k) =|s(n,n+1-k)|,其中s(n,k)是有符号的第一类斯特林数(1<=k<=n;换句话说,是倒序的无符号第一类斯特林数)。 1, 1, 1, 1, 3, 2, 1, 6, 11, 6, 1, 10, 35, 50, 24, 1, 15, 85, 225, 274, 120, 1, 21, 175, 735, 1624, 1764, 720, 1, 28, 322, 1960, 6769, 13132, 13068, 5040, 1, 36, 546, 4536, 22449, 67284, 118124, 109584, 40320, 1, 45, 870, 9450, 63273, 269325, 723680, 1172700
关于如何高效计算第一类斯特林数, 我不确定;维基百科给出了一个明确的公式,但看起来像是一个恶心的求和。这个问题(计算第一类斯特林数)在MathOverflow上出现,看起来像O(n^2),正如PengOne所猜测的那样。
基于 @PengOne 的答案,这里是我的JavaScript实现:链接
function g(N, L, R) {
var acc = 0;
for (var k=1; k<=N; k++) {
acc += comb(N-1, k-1) * f(k-1, L-1) * f(N-k, R-1);
}
return acc;
}
function f(N, L) {
if (N==L) return 1;
else if (N<L) return 0;
else {
var acc = 0;
for (var k=1; k<=N; k++) {
acc += comb(N-1, k-1) * f(k-1, L-1) * fact(N-k);
}
return acc;
}
}
function comb(n, k) {
return fact(n) / (fact(k) * fact(n-k));
}
function fact(n) {
var acc = 1;
for (var i=2; i<=n; i++) {
acc *= i;
}
return acc;
}
$("#go").click(function () {
alert(g($("#N").val(), $("#L").val(), $("#R").val()));
});
这是我受到@PengOne想法启发后的构建解决方案。
import itertools
def f(blocks, m):
n = len(blocks)
if m > n:
return []
if m < 0:
return []
if n == m:
return [sorted(blocks)]
maximum = max(blocks)
blocks = list(set(blocks) - set([maximum]))
results = []
for k in range(0, n):
for left_set in itertools.combinations(blocks, k):
for left in f(left_set, m - 1):
rights = itertools.permutations(list(set(blocks) - set(left)))
for right in rights:
results.append(list(left) + [maximum] + list(right))
return results
def b(n, l, r):
blocks = range(1, n + 1)
results = []
maximum = max(blocks)
blocks = list(set(blocks) - set([maximum]))
for k in range(0, n):
for left_set in itertools.combinations(blocks, k):
for left in f(left_set, l - 1):
other = list(set(blocks) - set(left))
rights = f(other, r - 1)
for right in rights:
results.append(list(left) + [maximum] + list(right))
return results
# Sample
print b(4, 3, 2) # -> [[1, 2, 4, 3], [1, 3, 4, 2], [2, 3, 4, 1]]
F(10, 4, 3)
,得出一个通用解决方案F(N, L, R)
。
( _ _ _ 10 _ _ _ _ _ _ )
。pos
的变量来跟踪N的位置。pos = 6 ( _ _ _ _ _ 10 _ _ _ _ )
。在10的左侧,有9C5 = (N-1)C(pos-1)
组数字需要排列。( _ _ 5 _ _ )
,并按照之前所做的类似步骤进行。( _ _ 4 _ )
。3 1 4 2
这样的序列,也可以计算像2 4 1 3
这样的序列F(4, 2, INF)
。pos == 6
时,排列的数量为9C5 * F(5, 3, INF) * F(4, 2, INF) = (N-1)C(pos-1) * F(pos-1, L-1, INF)* F(N-pos, R-1, INF)
。F(5, 3, INF)
中,5将在一系列具有L = 2
等的插槽中考虑。L = 1
时必须返回一个值,即F(N, 1, INF)
必须是一个基本情况。_ _ _ _ _ 6 7 10 _ _
。
F(5, 1, INF) = 4!
。F(N, 1, INF) = (N-1)!
。这里是一个用于测试代码的链接
#define INF UINT_MAX
long long unsigned fact(unsigned n) { return n ? n * fact(n-1) : 1; }
unsigned C(unsigned n, unsigned k) { return fact(n) / (fact(k) * fact(n-k)); }
unsigned F(unsigned N, unsigned L, unsigned R)
{
unsigned pos, sum = 0;
if(R != INF)
{
if(L == 0 || R == 0 || N < L || N < R) return 0;
if(L == 1) return F(N-1, R-1, INF);
if(R == 1) return F(N-1, L-1, INF);
for(pos = L; pos <= N-R+1; ++pos)
sum += C(N-1, pos-1) * F(pos-1, L-1, INF) * F(N-pos, R-1, INF);
}
else
{
if(L == 1) return fact(N-1);
for(pos = L; pos <= N; ++pos)
sum += C(N-1, pos-1) * F(pos-1, L-1, INF) * fact(N-pos);
}
return sum;
}
{1,2,3}
而不是{2,1,3}
。 - Cratylus{1,2,3}
从左边看有3
个可见块,从右边看有1
个可见块。 - PengOne2
的方块和高度为3
的方块,但是不会看到高度为1
的方块,因为它被高度为2
的方块挡住了。 - PengOne