【问题标题】:Number of Paths in a Triangle三角形中的路径数
【发布时间】:2015-06-18 05:42:40
【问题描述】:

我最近遇到了这个问题的一个更困难的变体,但我意识到我无法为这个非常简单的案例生成解决方案。我搜索了 Stack Overflow,但找不到以前回答过这个问题的资源。

给定一个三角形 ABC,您必须计算从“A”开始到“A”结束的特定长度路径的数量。假设我们的函数 f(3) 被调用,它必须返回从 A: 2 (ABA,ACA) 开始和结束的长度为 3 的路径数。

我无法制定一个优雅的解决方案。现在,我已经编写了一个生成所有可能路径的解决方案,但是对于更大的长度,程序太慢了。我知道必须有一个很好的动态编程解决方案,它可以重用我们之前计算过的序列,但我不太明白。非常感谢所有帮助。

我的愚蠢代码:

def paths(n,sequence):
    t = ['A','B','C']
    if len(sequence) < n:
        for node in set(t) - set(sequence[-1]):
            paths(n,sequence+node)
    else:
        if sequence[0] == 'A' and sequence[-1] == 'A':
            print sequence

【问题讨论】:

  • 你能举一个稍微大一点的三角形的例子吗?
  • 对不起,我不太明白。如果我在三角形上添加另一个点,那将是另一个循环图,只有 4 个点?我现在会尝试解决这个问题
  • 我的意思不是 4 分 - 只是您需要做的任何事情来显示您所描述的稍微“更大的长度”。
  • 抱歉。在 n = 21 的情况下,有 349526 条路径。我只是觉得必须有一些数学解决方案或技术来更快地计算它
  • 有,看看我的回答并尝试 n = 21,它给出 349526 作为输出

标签: algorithm recursion dynamic-programming combinatorics


【解决方案1】:

我的方法是这样的:

定义 DP(l, end) = # 路径结束于 end 并且长度为 l 那么DP(l,'A') = DP(l-1,'B') + DP(l-1,'C'),DP(l,'B')和DP(l,'C'类似)

然后对于基本情况即l = 1我检查结束是否不是'A',然后我返回0,否则返回1,这样所有更大的状态都只计算那些从'A'开始的状态

答案只是调用 DP(n, 'A') 其中 n 是长度

下面是 C++ 中的示例代码,你可以用 3 调用它,它会给你 2 作为答案;用 5 调用它,它会给你 6 作为答案: ABCBA、ACBCA、ABABA、ACACA、ABACA、ACABA

#include <bits/stdc++.h>
using namespace std;

int dp[500][500], n;

int DP(int l, int end){
	if(l<=0) return 0;
	if(l==1){
		if(end != 'A') return 0;
		return 1;
	}
	if(dp[l][end] != -1) return dp[l][end];
	
	if(end == 'A') return dp[l][end] = DP(l-1, 'B') + DP(l-1, 'C');
	else if(end == 'B') return dp[l][end] = DP(l-1, 'A') + DP(l-1, 'C');
	else return dp[l][end] = DP(l-1, 'A') + DP(l-1, 'B');
}

int main() {
	memset(dp,-1,sizeof(dp));
	scanf("%d", &n);
	
	printf("%d\n", DP(n, 'A'));
	return 0;
}

已编辑 在下面回答 OP 的评论:

首先,DP(动态编程)总是与状态有关。

请记住,这里的状态是 DP(l,end),表示长度为 l 并以 end 结束的路径数。所以使用编程实现状态,我们通常使用数组,所以DP[500][500]没有什么特别的,只是存储状态的空间DP(l,end)对于所有可能的lend(这就是为什么我说如果您需要更大的长度,请更改数组的大小)

但是你可能会问,我理解l的第一个维度,500意味着l可以大到500,但是第二个维度呢?我只需要'A','B','C',那为什么要用500呢?

这是另一个技巧(C/C++),char 类型确实可以默认用作int 类型,其值等于它的ASCII 数字。我当然不记得 ASCII 表,但我知道大约 300 个就足以表示所有 ASCII 字符,包括A(65)、B(66)、C(67)

所以我只是声明任何大到足以在第二维中表示“A”、“B”、“C”的大小(这意味着实际上 100 已经绰绰有余,但我只是不这么认为,并将 500 声明为就顺序而言,它们几乎相同)

所以你问 DP[3][1] 是什么意思,它没有任何意义,因为当它是 1 时我不需要/计算第二维。(或者可以认为状态 dp(3,1) 确实在我们的问题中没有任何物理意义)

事实上,我一直使用 65、66、67。 所以 DP[3][65] 表示长度为 3 并以 char(65) = 'A' 结束的路径数

【讨论】:

  • 我想我理解这种方法:以 A 结尾的路径数等于以 B 或 C 结尾的长度为 l-1 的路径数,依此类推。但是 dp[500][500] 信息存储是什么 - dp[3][1] 是什么意思?
  • 知道了。所以只是为了理解,DP数组只使用dp[x][65]、dp[x][66]和dp[x][67](0
  • 是的,你没看错。所以对于你需要的更大的 x,增加第一维的大小。当然你总是可以把第二维改小到3,用0,1,2来代表A,B,C
  • 由于对称性的问题,很容易看出DP(n, 'B') = DP(n, 'C')。因此,公式可以简化为DP(n, 'A') = 2 * DP(n - 1, 'B'); DP(n, 'B') = DP(n - 1, 'A') + DP(n - 1, 'B');
【解决方案2】:

诀窍是不要尝试生成所有可能的序列。它们的数量呈指数增长,因此所需的内存太大。

相反,让f(n) 是长度为n 的序列数,以A 开头和结尾,并让g(n) 为长度为n 的序列数,以A 开头但以@ 结尾987654327@。要开始,请明确f(1) = 1g(1) = 0。对于n &gt; 1,我们有f(n) = 2g(n - 1),因为倒数第二个字母将是BC,并且每个字母的数量相同。我们还有g(n) = f(n - 1) + g(n - 1),因为如果一个序列以A 开头并以B 结尾,倒数第二个字母是AC

这些规则允许您使用记忆快速计算数字。

【讨论】:

  • 我也会赞成这个答案,但我没有声誉,对不起:(。这个解释非常简洁,帮助我理解了另一个答案。
  • @SimpleLearner 不用担心。我很高兴你找到了答案:) 会有一个简单的公式来解决这个问题。计算出前六个左右数字的 f(n),将它们输入到整数序列在线百科全书中,就会出现一个公式。
  • 哇。那真是太棒了。显然等式是 (1-x)/(1 - x - 2*x^2)。右边列出的是:“在三角形的同一顶点开始和结束的封闭步行”。现在来弄清楚他们是如何发现的!
  • @SimpleLearner “方程”是方程 a_{n+2} = a_{n+1} + 2a_{n} 的递归关系的生成函数,具有适当的初始条件。跨度>
【解决方案3】:

PA(n) 是从 A 返回到 A 的路径数,恰好 n 步。 设P!A(n) 是从 B(或 C)到 A 的路径数,恰好是 n 步。

然后:

PA(1) = 1
PA(n) = 2 * P!A(n - 1)

P!A(1) = 0
P!A(2) = 1
P!A(n) = P!A(n - 1) + PA(n - 1)
       = P!A(n - 1) + 2 * P!A(n - 2) (for n > 2) (substituting for PA(n-1))

我们可以解析解 P!A 的差分方程,就像我们对 Fibonacci 所做的那样,注意 (-1)^n 和 2^n 都是差分方程的解,然后找到系数 a, b 这样即 P!A(n) = a*2^n + b*(-1)^n。

我们最终得到等式 P!A(n) = 2^n/6 + (-1)^n/3,PA(n) 为 2^(n-1)/3 - 2(- 1)^n/3.

这给了我们代码:

def PA(n):
    return (pow(2, n-1) + 2*pow(-1, n-1)) / 3

for n in xrange(1, 30):
    print n, PA(n)

提供输出:

1 1
2 0
3 2
4 2
5 6
6 10
7 22
8 42
9 86
10 170
11 342
12 682
13 1366
14 2730
15 5462
16 10922
17 21846
18 43690
19 87382
20 174762
21 349526
22 699050
23 1398102
24 2796202
25 5592406
26 11184810
27 22369622
28 44739242
29 89478486

【讨论】:

  • 我花了一段时间才理解递归关系,但我想我现在明白了。 P!A(n) = P!A(n - 1) + PA(n - 1) 计算从 B 到 A 的路径: 1) P!A(n-1) 计算从 C 开始并在 C 结束的路径长度为 n-1 的 A 但源自 B 以获得 n 长度。 2) PA(n - 1) 计算从 A 到 A 的长度为 n-1 且源自 B 的路径。
  • 这个解决方案对您来说是显而易见的吗?如果有,有没有什么解决问题的书籍或问题对你有帮助?
  • @SimpleLearner 请参阅关于斐波那契数的维基百科页面,了解几乎相同的理论(尽管这个系列的结果更好一些,因为二次方的根是整数)。此外,Sedgewick 和 Flageolet “算法分析简介”涵盖了计算离散结构的一般技术。
【解决方案4】:

我们可以通过 for 循环来解决这个问题,尽管 Anonymous 为它描述了一个封闭的形式。

function f(n){
  var as = 0, abcs = 1;

  for (n=n-3; n>0; n--){
    as = abcs - as;
    abcs *= 2;
  }

  return 2*(abcs - as);
}

原因如下:

Look at one strand of the decision tree (the other one is symmetrical):

                                        A

                  B                                            C...
     A                        C
B         C               A        B
A    C    A    B          B   C    A   C
B C  A B  B C  A C        A C A B  B C A B


Num A's     Num ABC's (starting with first B on the left)
0            1
1 (1-0)      2
1 (2-1)      4
3 (4-1)      8
5 (8-3)      16
11 (16-5)    32

Cleary, we can't use the strands that end with the A's...

【讨论】:

    【解决方案5】:

    对于给定的三角形和更一般的图形,您可以比其他人发布的动态编程/递归解决方案做得更好。每当您尝试计算(可能是有向的)图中的行走次数时,您都可以用传递矩阵的幂项来表示。令 M 是一个矩阵,其条目 m[i][j] 是从顶点 i 到顶点 j 的长度为 1 的路径的数量。对于三角形,传递矩阵是

    0 1 1
    1 0 1.
    1 1 0
    

    那么 M^n 是一个矩阵,它的 i,j 项是从顶点 i 到顶点 j 的长度为 n 的路径的数量。如果 A 对应顶点 1,则需要 M^n 的 1,1 条目。

    根据长度为 n-1 的路径计算长度为 n 的路径计数的动态规划和递归等价于用 n 次乘法计算 M^n,M * M * M * ... * M,它可以足够快。但是,如果你想计算 M^100,而不是做 100 次乘法,你可以使用repeated squaring:计算 M,M^2,M^4,M^8,M^16,M^32,M^64 ,然后是 M^64 * M^32 * M^4。对于较大的指数,乘法的数量约为 c log_2(exponent)。

    不是使用长度为 n 的路径由长度为 n-1 的路径和长度为 1 的步长组成,而是使用长度为 n 的路径由长度为 k 的路径组成,然后一条长度为 nk 的路径。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-08
      相关资源
      最近更新 更多