• 3. 博弈树 (15分)


    下棋属于一种博弈游戏,博弈过程可以用树(博弈树)来表示。假设游戏由两个人( A 和 B )玩,开始由某个人从根结点开始走,两个人轮流走棋,每次只能走一步, 下一步棋只能选择当前结点的孩子结点,谁先走到叶子结点为胜。例如,对于下图所示的博弈树,若 A 先走,可以选 f , B 若选 h ,则 A 选 j 胜。

    编写一程序,让计算机和人下棋。当计算机走下一步时,可以根据以下情况决定下一步:

    (1) 若存在可以确保取胜的一个孩子结点,则选择该结点作为下一步;

    (2) 若存在多个可以确保取胜的孩子结点,则选择其中高度最小的结点作为下一步(若有多个选择,则选最左边的结点);

    (3) 若不存在可以确保取胜的一个孩子结点,则选择高度最大的孩子结点作为下一步(若有多个选择,则选最左边的结点);

    例: (下面的黑体为输入)

    (a,(b,(x)),(c,(d),(e,(g),(h)),(f)))

    a

    b

    x

    c

    d

    e

    g

    h

    f

    Who play first(0: computer; 1: player )?

    1

    player:

    c

    computer: d

    Sorry, you lost.

    Continue(y/n)?

    y

    Who play first(0: computer; 1: player )?

    1

    player:

    x

    illegal move.

    player:

    b

    computer: x

    Sorry, you lost.

    Continue(y/n)?

    y

    Who play first(0: computer; 1: player )?

    0

    computer: c

    player:

    f

    Congratulate, you win.

    Continue(y/n)?

    n

    测试输入期待的输出时间限制内存限制额外进程
    测试用例 1以文本方式显示
    1. (a,(b,(x)),(c,(d),(e,(g),(h)),(f)))↵
    2. 1↵
    3. c↵
    4. y↵
    5. 1↵
    6. x↵
    7. b↵
    8. y↵
    9. 0↵
    10. f↵
    11. n↵
    以文本方式显示
    1. a↵
    2. b↵
    3. x↵
    4. c↵
    5. d↵
    6. e↵
    7. g↵
    8. h↵
    9. f↵
    10. Who play first(0: computer; 1: player )?↵
    11. player:↵
    12. computer: d↵
    13. Sorry, you lost.↵
    14. Continue(y/n)?↵
    15. Who play first(0: computer; 1: player )?↵
    16. player:↵
    17. illegal move.↵
    18. player:↵
    19. computer: x↵
    20. Sorry, you lost.↵
    21. Continue(y/n)?↵
    22. Who play first(0: computer; 1: player )?↵
    23. computer: c↵
    24. player:↵
    25. Congratulate, you win.↵
    26. Continue(y/n)?↵
    1秒64M0
    测试用例 2以文本方式显示
    1. (a,(b,(c,(d),(e)),(f)),(g,(h),(i)),(j,(k,(m),(n),(o),(p,(r))),(x,(y,(z)))))↵
    2. 1↵
    3. j↵
    4. y↵
    5. y↵
    6. 1↵
    7. b↵
    8. y↵
    9. 0↵
    10. k↵
    11. n↵
    以文本方式显示
    1. a↵
    2. b↵
    3. c↵
    4. d↵
    5. e↵
    6. f↵
    7. g↵
    8. h↵
    9. i↵
    10. j↵
    11. k↵
    12. m↵
    13. n↵
    14. o↵
    15. p↵
    16. r↵
    17. x↵
    18. y↵
    19. z↵
    20. Who play first(0: computer; 1: player )?↵
    21. player:↵
    22. computer: x↵
    23. player:↵
    24. computer: z↵
    25. Sorry, you lost.↵
    26. Continue(y/n)?↵
    27. Who play first(0: computer; 1: player )?↵
    28. player:↵
    29. computer: f↵
    30. Sorry, you lost.↵
    31. Continue(y/n)?↵
    32. Who play first(0: computer; 1: player )?↵
    33. computer: j↵
    34. player:↵
    35. computer: m↵
    36. Sorry, you lost.↵
    37. Continue(y/n)?↵
    1秒64M0
    测试用例 3以文本方式显示
    1. (a,(b,(c,(d),(e)),(f)),(g,(h),(i)),(q,(k,(m),(n),(o),(p,(r))),(x,(y,(z)))))↵
    2. 1↵
    3. q↵
    4. x↵
    5. y↵
    6. n↵
    以文本方式显示
    1. a↵
    2. b↵
    3. c↵
    4. d↵
    5. e↵
    6. f↵
    7. g↵
    8. h↵
    9. i↵
    10. q↵
    11. k↵
    12. m↵
    13. n↵
    14. o↵
    15. p↵
    16. r↵
    17. x↵
    18. y↵
    19. z↵
    20. Who play first(0: computer; 1: player )?↵
    21. player:↵
    22. computer: x↵
    23. player:↵
    24. illegal move.↵
    25. player:↵
    26. computer: z↵
    27. Sorry, you lost.↵
    28. Continue(y/n)?↵
    1秒64M0

    博弈树学习笔记:

    完全博弈问题--下棋

    特点:

        双人对弈: 轮流下,一人走一步.

        信息完备: 双方看到的信息一样.

        零和: 双方利益冲突,对一方有利则对另一方不利。

    极小极大搜索方法: (假定对手每次回应都正确,即选择MIN)

        假定有一个评价函数可以对所有的棋局进行评估.

        评价函数值大于0,棋局对我方有利,对对方不利,反之同理.

        对节点N取估价函数f(N),分两类节点:

            Max,有选择时取最大;

            Min,有选择时取最小.

    具体思路:

        1.我方走棋时,首先按照一定的搜索深度生成出给定深度d以内的所有状态,计算所有叶节点的评价函数值, 然后从d-1层节点开始逆向计算.

        2.对于我方要走的节点(MAX),取其子节点中的最大值为该节点的值(选择对我方有利的棋)。

        3.对于对方要走的节点(MIN),取其子节点中的最小值为该节点的值(选择对我方不利的棋)。

        4.直到计算出根节点的值为止,获得根节点取值的分枝即为最佳选择.

        5.对方回应一步棋后,重新演算.

    显然这样做太低效了,需要剪枝,分析:

    α-β搜索方法定义:(其实就是给节点定义范围区间--[α,β],初始化为+-∞)

        Max节点的下界为α,Min节点的上界为β.

        极大节点N,从一个子树获得的α值和β值,可以传送给其他子节点

        极大节点N的α值只可能越改越大,极小节点M的β值只可能越改越小.

        从单边子节点往父节点推,极大值父节点只更改α值,极小值父节点只更改β值.

    α剪枝(发生在极小层节点)---从一个子树获得的极大节点的α值,可以传送给该节点的其他子树,从而选择α剪枝机会

        若对任一极小值层节点,α(任意父辈层)≥β(后继层),则可中止此MIN节点以下的搜索过程.

        此MIN节点的最终倒推值就确定为此β值.

    β剪枝(发生在极大层节点)---从一个子树获得的极小节点的β值,可以传送给该节点的其他子节点,从而选择β剪枝机会

        若对任一极大值层节点,α(后继层)≥β(任意父辈层),则可中止此MAX节点以下的搜索过程.

        此MAX节点的最终倒推值就确定为此α值。

    代码如下:

            吐槽:晚上写完感觉逻辑没问题终于交了,结果还是WA,后来发现是输出少了空格(这一句:Who play first(0: computer; 1: player )?);改了之后AC了,,,但是看了看报表,看到一个1378B,22行过的,于是我看着我9492B,391行的代码陷入了沉思(嘶,不是哥们,这我怎么睡的着啊?就22行,我是真想不明白,除了printf/cout还能怎么写,毕竟这次测试用例是给出的三个,没有隐藏)

            强调尽量不要格式化文档,vscode的格式化文档害我WA好多次┭┮﹏┭┮,对了,现在只有6755B 303行了(合并了一些函数,去掉了一些实际上需要但是这道题测试用例考不到的检错代码)

            吃个饭回来写注释。

            注释写了,但是习惯性格式化了,现在输出不一定能过测试用例了应该,估计会少或者多个空格之类的。

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. const int INF = 0x3f3f3f3f;
    7. typedef enum // 列表元素的类别
    8. {
    9. ATOM,
    10. LIST
    11. } ElemClass;
    12. typedef struct GLNode
    13. {
    14. ElemClass Class; // 元素的类别
    15. union
    16. {
    17. char atom; // 原子值
    18. struct
    19. {
    20. GLNode *head; // 子列表的头指针
    21. GLNode *tail; // 子列表的尾指针
    22. } ptr;
    23. };
    24. } *Glist;
    25. typedef struct Node
    26. {
    27. char data; // 节点存储数据
    28. int status; // 预处理状态标志
    29. struct Node *first; // 指向第一个子节点的指针
    30. struct Node *next; // 指向下一个兄弟节点的指针
    31. } *Tree;
    32. int CreateGlist(Glist &L, string s)
    33. {
    34. if (s == "()")
    35. L = NULL; // 将广义表指针设为NULL,表示空列表
    36. else
    37. {
    38. L = new GLNode; // 创建一个新的广义表节点
    39. if (s.length() == 1)
    40. {
    41. L->Class = ATOM; // 设置节点类别为原子
    42. L->atom = s[0]; // 将原子值存储在节点中
    43. }
    44. else
    45. {
    46. L->Class = LIST; // 设置节点类别为列表
    47. string sub, headsub; // sub表示子串,headsub表示头部子串
    48. sub = s.substr(1, s.size() - 2); // 去除外层括号,得到子串
    49. Glist p, q; // p表示当前节点,q表示上一个节点
    50. p = L;
    51. while (!sub.empty())
    52. {
    53. int i, k = 0;
    54. int n = sub.length();
    55. char c = '\0';
    56. for (i = 0; i < n && (c != ',' || k != 0); i++) // 查找子串中的逗号
    57. {
    58. c = sub[i];
    59. k += (c == '(') ? 1 : ((c == ')') ? -1 : 0); // 统计括号的个数
    60. }
    61. headsub = (i < n) ? sub.substr(0, i - 1) : sub; // 获取头部子串
    62. sub = (i < n) ? sub.substr(i, n - i) : ""; // 更新子串,去除已处理的部分
    63. CreateGlist(p->ptr.head, headsub); // 递归创建子列表(或者说是更新)
    64. q = p;
    65. if (!sub.empty())
    66. {
    67. p = new GLNode; // 创建新的广义表节点
    68. p->Class = LIST;
    69. q->ptr.tail = p; // 将新节点连接到上一个节点的尾部
    70. }
    71. }
    72. q->ptr.tail = NULL; // 最后一个节点的尾部设为NULL
    73. }
    74. }
    75. return 1; // 返回创建成功的标志
    76. }
    77. void CreateTree(Tree &root, Glist L)
    78. {
    79. if (L) // 如果广义表不为空
    80. {
    81. root = new Node; // 创建一个新的节点
    82. root->data = L->ptr.head->atom; // 将广义表头部的原子值存储在节点中
    83. root->first = NULL; // 初始化节点的第一个孩子和下一个兄弟节点为NULL
    84. root->next = NULL;
    85. if (L->ptr.tail) // 如果广义表有尾部,即有子列表
    86. {
    87. Glist head = L->ptr.tail->ptr.head; // 获取尾部的头部子列表
    88. Glist t = L->ptr.tail->ptr.tail; // 获取尾部的尾部子列表
    89. CreateTree(root->first, head); // 递归创建第一个孩子节点
    90. Node *p = root->first; // 指向当前孩子节点
    91. while (t) // 遍历尾部的剩余子列表
    92. {
    93. head = t->ptr.head; // 获取当前子列表的头部
    94. t = t->ptr.tail; // 获取当前子列表的尾部
    95. CreateTree(p->next, head); // 递归创建下一个兄弟节点
    96. p = p->next; // 移动到下一个兄弟节点
    97. }
    98. p->next = NULL; // 最后一个兄弟节点的下一个节点设为NULL
    99. }
    100. }
    101. else
    102. root = NULL; // 如果广义表为空,树的根节点设为NULL
    103. }
    104. int PreTreat(Tree root)
    105. {
    106. if (!root) // 树为空
    107. return 1; // 返回1,表示预处理完成
    108. if (!root->first) // 如果树的第一个孩子为空
    109. root->status = 1; // 将根节点的状态设为1
    110. PreTreat(root->first); // 递归预处理根节点的第一个孩子节点
    111. PreTreat(root->next); // 递归预处理根节点的下一个兄弟节点
    112. if (root->first) // 如果根节点有第一个孩子节点
    113. {
    114. Node *p = root->first; // 指向第一个孩子节点
    115. int flag = 1; // 标志位,初始设为1
    116. while (p) // 遍历根节点的孩子节点
    117. {
    118. if (p->status == 1) // 如果孩子节点的状态为1
    119. {
    120. flag = 0; // 将标志位设为0
    121. break;
    122. }
    123. p = p->next; // 移动到下一个兄弟节点
    124. }
    125. root->status = flag; // 将根节点的状态设为标志位的值
    126. }
    127. return 1; // 返回1,表示预处理完成
    128. }
    129. int GetHeight(Tree root)
    130. {
    131. if (root == NULL) // 如果树为空
    132. return 0; // 返回高度为0
    133. else
    134. {
    135. int head, h2; // head表示第一个孩子节点的高度,h2表示兄弟节点的高度
    136. head = GetHeight(root->first); // 递归获取第一个孩子节点的高度
    137. if (head != 0) // 如果第一个孩子节点的高度不为0
    138. {
    139. root = root->first; // 将根节点指向第一个孩子节点
    140. while (root) // 遍历兄弟节点
    141. {
    142. h2 = GetHeight(root->next); // 递归获取兄弟节点的高度
    143. if (head < h2) // 如果兄弟节点的高度大于第一个孩子节点的高度
    144. head = h2; // 更新第一个孩子节点的高度为更大的值
    145. root = root->next; // 移动到下一个兄弟节点
    146. }
    147. }
    148. return head + 1; // 返回树的高度(第一个孩子节点的高度加1)
    149. }
    150. }
    151. Tree Computer(Tree root)
    152. {
    153. int win = INF; // 最小值,用于记录最小高度
    154. int lose = 0; // 最大值,用于记录最大高度
    155. Tree p = root->first; // 指向第一个孩子节点
    156. Tree q1, q2; // 用于记录选中的节点
    157. while (p) // 遍历孩子节点
    158. {
    159. int head = GetHeight(p); // 获取当前孩子节点的高度
    160. if (p->status == 1) // 如果孩子节点状态为1(表示胜利的情况)
    161. {
    162. if (height < win) // 如果当前孩子节点的高度小于最小高度
    163. {
    164. win = height; // 更新最小高度
    165. q1 = p; // 记当前节点为选中节点
    166. }
    167. }
    168. else // 如果孩子节点状态为0(表示失败的情况)
    169. {
    170. if (height > lose) // 如果当前孩子节点的高度大于最大高度
    171. {
    172. lose = height; // 更新最大高度
    173. q2 = p; // 记当前节点为选中节点
    174. }
    175. }
    176. p = p->next; // 移动到下一个兄弟节点
    177. }
    178. if (win != INF) // 如果存在胜利的情况
    179. {
    180. cout << "computer: " << q1->data << endl; // 输出选中节点的数据
    181. return q1; // 返回选中节点
    182. }
    183. else // 如果不存在胜利的情况
    184. {
    185. cout << "computer: " << q2->data << endl; // 输出选中节点的数据
    186. return q2; // 返回选中节点
    187. }
    188. return root; // 如果无法确定选中节点,则返回根节点
    189. }
    190. Tree Human(Tree root)
    191. {
    192. cout << "player:" << endl;
    193. char c;
    194. cin.ignore(); // 忽略之前的输入缓冲,去除回车
    195. cin >> c; // 获取玩家输入的字符
    196. Node *p = root->first; // 指向第一个孩子节点
    197. while (p) // 遍历孩子节点
    198. {
    199. if (p->data == c) // 如果节点的数据等于玩家输入的字符
    200. return p; // 返回该节点作为玩家选中的节点
    201. p = p->next; // 移动到下一个兄弟节点
    202. }
    203. cout << "illegal move." << endl; // 如果没有匹配的节点,输出非法移动提示
    204. return root; // 返回根节点
    205. }
    206. Tree ValidMove(Tree p)
    207. {
    208. while (1) // 无限循环,直到玩家选择一个有效的移动
    209. {
    210. Tree q; // 用于存储玩家选择的节点
    211. q = Human(p); // 玩家进行选择,获取玩家选中的节点
    212. if (q != p) // 如果玩家选择的节点与当前节点不相同(表示有效移动)
    213. return q; // 返回玩家选择的节点
    214. }
    215. }
    216. int Move(Tree root)
    217. {
    218. while (true) // 无限循环,直到玩家选择不继续游戏
    219. {
    220. cout << "Who play first(0: computer; 1: player )?" << endl; // 提示选择谁先玩
    221. int player, flag = 0; // player表示玩家选择的角色,flag用于标记当前玩家是电脑还是玩家
    222. cin >> player; // 获取玩家选择的角色
    223. Tree p; // 用于存储当前节点
    224. if (player == 0) // 如果玩家选择电脑先玩
    225. {
    226. p = Computer(root); // 电脑进行移动选择
    227. flag = 0; // 标记当前玩家为电脑
    228. }
    229. else // 如果玩家选择玩家先玩
    230. {
    231. p = Human(root); // 玩家进行移动选择
    232. flag = 1; // 标记当前玩家为玩家
    233. }
    234. if (p->first == nullptr) // 如果当前节点没有孩子节点(表示游戏结束)
    235. {
    236. if (flag == 0) // 如果当前玩家为电脑
    237. cout << "Sorry, you lost." << endl; // 输出玩家失败的信息
    238. else
    239. cout << "Congratulate, you win." << endl; // 输出玩家胜利的信息
    240. cout << "Continue(y/n)?" << endl; // 询问是否继续游戏
    241. char c;
    242. cin >> c; // 获取玩家选择
    243. if (c == 'y') // 如果玩家选择继续游戏
    244. continue; // 继续下一轮游戏
    245. else
    246. return 1; // 结束游戏
    247. }
    248. while (true) // 内层循环,直到游戏结束
    249. {
    250. if (flag == 1 && p != root) // 如果上一步为玩家且不是根节点
    251. {
    252. p = Computer(p); // 电脑进行移动选择
    253. if (p->first == nullptr) // 如果当前节点没有孩子节点(表示游戏结束)
    254. {
    255. cout << "Sorry, you lost." << endl; // 输出玩家失败的信息
    256. break;
    257. }
    258. p = ValidMove(p); // 玩家进行移动选择
    259. if (p->first == nullptr)
    260. {
    261. cout << "Congratulate, you win." << endl;
    262. break;
    263. }
    264. }
    265. else // 如果上一步为电脑或者是根节点
    266. {
    267. p = ValidMove(p); // 玩家进行移动选择
    268. if (p->first == nullptr) // 表示游戏结束
    269. {
    270. cout << "Congratulate, you win." << endl;
    271. break;
    272. }
    273. p = Computer(p); // 电脑进行移动选择
    274. if (p->first == nullptr) // 表示游戏结束
    275. {
    276. cout << "Sorry, you lost." << endl;
    277. break;
    278. }
    279. }
    280. }
    281. cout << "Continue(y/n)?" << endl;
    282. char c;
    283. cin >> c;
    284. if (c == 'y')
    285. continue; // 继续下一轮游戏
    286. else
    287. return 1; // 结束游戏
    288. }
    289. return 1;
    290. }
    291. int main()
    292. {
    293. string s;
    294. cin >> s;
    295. int i;
    296. for (i = 0; s[i] != '\0'; i++)
    297. if (s[i] >= 'a' && s[i] <= 'z')
    298. cout << s[i] << endl;
    299. Glist L;
    300. CreateGlist(L, s);
    301. Tree root;
    302. CreateTree(root, L);
    303. PreTreat(root);
    304. Move(root);
    305. return 0;
    306. }

  • 相关阅读:
    Day17-购物车页面-商品列表-实现滑动删除功能
    遥感影像正射矫正
    基于JavaSwing开发3D立体字制作程序 课程设计 大作业
    Bean的作用域和生命周期
    论文笔记:Leveraging Language Foundation Models for Human Mobility Forecasting
    MySQL增删查改(初阶)
    1.4+1.5 L1、L2正则化
    电脑工作者缓解眼部疲劳问题的工具分享
    随机过程理论知识(六)
    东营市高新技术企业认定8个必须知道的问题
  • 原文地址:https://blog.csdn.net/Cosmo9/article/details/134046997