算法 - 比賽得分可能數


算法 - 比賽得分可能數


1 介紹

2 實現

        2.1 分析

        2.2 算法一

        2.3 算法二

        2.4 算法三


1 介紹

以排球比賽分例,默認情況下,A、B 兩隊比賽時,率先得到 25 分得將取勝。但若雙方打成 24:24 時,則最高得分將變為 26 分。以后情況以此類推,例如,理論上可以打出諸如 40:42 這樣的結局。

算法的要求是,在已知最終比分的情況下,算出能夠達到該最終比分的所有可能得分順序。

例1:如果最終比分是 0:25,那么只有一種順序,那就是:

0:1 → 0:2 → 0:3 → ... 0:24 → 0:25

例2:如果最終比分是 1:25,那么可能有 25 種情況,例如:

1:0 → 0:1 → 0:2 → 0:3 → ... 0:24 → 0:25

0:1 → 1:1 → 1:2 → 1:3 → ... 1:24 → 1:25

0:1 → 0:2 → 1:2 → 1:3 → ... 1:24 → 1:25

......

例3:如果最終比分是 3:25,將有 2925 種情況

例4:如果最終比分是 3:12,結果為 0,因為不可能有這種情況的最終比分

要求編寫程序實現該功能,可以支持的最大比分數為 40。

2 實現

2.1 分析

很容易想到,比賽的可能路徑可以使用如下的二叉樹來分析,紅色節點表示為不可能的分支

 

本例中的算法使用 C# 實現

首先不管算法如何,先把一些公共的判斷函數寫在這里,以后其它例子都會用

// 是否有效最終比分
const int MAX = 25; static bool IsValid(int A, int B) { if (A < MAX && B < MAX) { return false; } if (Math.Abs(A - B) < 2) { return false; } if (Math.Max(A, B) > MAX && Math.Abs(A - B) != 2) { return false; } return true; } // 是否為有效中間比分,例如如果最終比分為 3:25,中間不可能出現 2:25(已經贏了不用再打了) static bool IsValidPath(int A, int B, int finalA, int finalB) { if (A > finalA || B > finalB) { return false; } if (IsWinStatus(A, B)) { return A == finalA && B == finalB; } else { return true; } } // 是否獲勝 static bool IsWinStatus(int A, int B) { if (Math.Max(A, B) == MAX && Math.Abs(A - B) >= 2) { return true; } if (Math.Max(A, B) > MAX && Math.Abs(A - B) == 2) { return true; } return false; }

2.2 算法一

很容易想到第一種算法,就是試圖生成上圖中的二叉樹,如果生成出一個最終葉子結點(例如 3:25)則將計數器加 1,如下所示

class Model1 { static void Main(string[] args) { int a = CalcScore1(7, 25); Console.WriteLine("Result: " + a); Console.Read(); } // 計算可能得分的入口函數 static int CalcScore1(int A, int B) { if (!IsValid(A, B)) { return 0; } Node rootNode = new Node(0, 0); int counter = 0; Constuct(rootNode, A, B, ref counter); return counter; } private static void Constuct(Node node, int finalA, int finalB, ref int counter) { if (node.ValueA == finalA && node.ValueB == finalB) { counter++; } else { if (node.ValueA < finalA && IsValidPath(node.ValueA + 1, node.ValueB, finalA, finalB)) { node.Left = new Node(node.ValueA + 1, node.ValueB); } if (node.ValueB < finalB && IsValidPath(node.ValueA, node.ValueB + 1, finalA, finalB)) { node.Right = new Node(node.ValueA, node.ValueB + 1); } } if (node.Left != null) { Constuct(node.Left, finalA, finalB, ref counter); } if (node.Right != null) { Constuct(node.Right, finalA, finalB, ref counter); } } class Node { public Node(int A, int B) { this.ValueA = A; this.ValueB = B; } public int ValueA { get; set; } public int ValueB { get; set; } public Node Left { get; set; } public Node Right { get; set; } } }

但結果很殘酷,計算 3:25 時尚可,但計算到 7:25 時內存就溢出了

2.3 算法二

為了改進空間使用,我試着放棄使用二叉樹結構,因為我們並不需要存儲整棵樹,只需要存儲所有的未到達最終比分的最后一層中間結點即可。例如,找到 (1:0) 和 (0:1) 后,(0:0) 結點即可拋棄。

改進后的算法如下,這次不需要遞歸:

static int CalcScore(int A, int B) { if (!IsValid(A, B)) { return 0; } long counter = 0; List<int[]> list = new List<int[]>(); list.Add(new int[] { 0, 0 }); while (true) { // check final status for (int i = list.Count - 1; i >= 0; i--) { if (IsWinStatus(list[i][0], list[i][1])) { counter++; list.RemoveAt(i); } } // calc next level List<int[]> list2 = new List<int[]>(); foreach (int[] score in list) { int[] newScore1 = new int[] { score[0] + 1, score[1] }; int[] newScore2 = new int[] { score[0], score[1] + 1 }; if (IsValidPath(newScore1[0], newScore1[1], A, B)) { list2.Add(newScore1); } if (IsValidPath(newScore2[0], newScore2[1], A, B)) { list2.Add(newScore2); } } if (list2.Count == 0) { break; } list = list2; } return counter; }

結果嘛稍好了一丟丟,可以算出 (8:25),再往下也不行了。

2.4 算法三

仔細再分析一下上面的過程,我們可以列出如下的式子

f(0:0) = f(1:0) + f(0:1)
       = f(2:0) + f(1:1) + f(1:1) + f(0:2) # 相當於 f(2:0) + f(1:1) * 2 + f(0:2)
       = f(3:0) + f(2:1) + f(2:1) + f(1:2) + f(2:1) + f(1:2) + f(1:2) + f(0:3) # 相當於 f(3:0) + f(2:1) * 3 + f(1:2) * 3 + f(0:3)
       = ....

我們不需要存儲任何中點結點,只需要算數就行了,這樣能節省些空間。更重要的是,很多條路徑的結果是需要多次使用的,我們不需要每次都計算一遍。

本着該思路,我寫出了如下的算法:

static ulong CalcScore3(int A, int B) { if (!IsValid(A, B)) { return 0; } Dictionary<Tuple<int, int>, ulong> nodeDic = new Dictionary<Tuple<int, int>, ulong>(); ulong counter = CalcPathCount(0, 0, A, B, nodeDic); return counter; } static ulong CalcPathCount(int A, int B, int finalA, int finalB, Dictionary<Tuple<int, int>, ulong> nodeDic) { Tuple<int, int> key = new Tuple<int, int>(A, B); if (nodeDic.ContainsKey(key)) { return nodeDic[key]; } else { if (!IsValidPath(A, B, finalA, finalB)) { nodeDic.Add(key, 0); return 0; } if (IsWinStatus(A, B)) { nodeDic.Add(key, 1); return 1; } Tuple<int, int> keyLeft = new Tuple<int, int>(A + 1, B); Tuple<int, int> keyRight = new Tuple<int, int>(A, B + 1); ulong pathCount = CalcPathCount(A + 1, B, finalA, finalB, nodeDic) + CalcPathCount(A, B + 1, finalA, finalB, nodeDic); nodeDic.Add(key, pathCount); return pathCount; } }

至此問題解決了, 結果如下所示:

 0:25 - 1
 1:25 - 25
 2:25 - 325
 3:25 - 2925
 4:25 - 20475
 5:25 - 118755
 6:25 - 593775
 7:25 - 2629575
 8:25 - 10518300
 9:25 - 38567100
10:25 - 131128140
11:25 - 417225900
12:25 - 1251677700
13:25 - 3562467300
14:25 - 9669554100
15:25 - 25140840660
16:25 - 62852101650
17:25 - 151584480450
18:25 - 353697121050
19:25 - 800472431850
20:25 - 1761039350070
21:25 - 3773655750150
22:25 - 7890371113950
23:25 - 16123801841550
24:26 - 32247603683100
25:27 - 64495207366200
26:28 - 128990414732400
27:29 - 257980829464800
28:30 - 515961658929600
29:31 - 1031923317859200
30:32 - 2063846635718400
31:33 - 4127693271436800
32:34 - 8255386542873600
33:35 - 16510773085747200
34:36 - 33021546171494400
35:37 - 66043092342988800
36:38 - 132086184685977600
37:39 - 264172369371955200
38:40 - 528344738743910400
39:41 - 1056689477487820800
40:42 - 2113378954975641600
41:43 - 4226757909951283200
42:44 - 8453515819902566400
43:45 - 16907031639805132800

Index


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM