@@ -374,7 +374,7 @@ bool GameNodeRep::IsSuccessorOf(GameNode p_node) const
374374bool GameNodeRep::IsSubgameRoot () const
375375{
376376 if (m_children.empty ()) {
377- return false ;
377+ return ! GetParent () ;
378378 }
379379 return m_game->GetSubgameRoot (m_infoset->shared_from_this ()) == shared_from_this ();
380380}
@@ -1066,23 +1066,30 @@ void GameTreeRep::BuildUnreachableNodes()
10661066namespace { // Anonymous namespace
10671067
10681068// Comparator for priority queue (ancestors have lower priority than descendants)
1069+ // This ensures descendants are processed first, allowing upward traversal to find
1070+ // the highest reachable node in each component
10691071struct NodeCmp {
10701072 bool operator ()(const GameNodeRep *a, const GameNodeRep *b) const
10711073 {
10721074 return a->GetNumber () < b->GetNumber ();
10731075 }
10741076};
10751077
1076- // Scratch data structure for subgame root finding algorithm
10771078struct SubgameScratchData {
1078- // DSU structures
1079+ // / DSU structures
1080+ // /
1081+ // / Maps each infoset to its parent in the union-find structure.
1082+ // / After path compression, points to the component leader directly.
10791083 std::unordered_map<GameInfosetRep *, GameInfosetRep *> dsu_parent;
1084+ // / Maps each component leader to the highest node (candidate root) in that component
10801085 std::unordered_map<GameInfosetRep *, GameNodeRep *> subgame_root_candidate;
1081- // Timestamps for nodes
1082- std::vector<int > node_visited_token;
1083- int current_token = 0 ;
10841086
1085- // DSU Find with path compression
1087+ // / DSU Find + path compression: Finds the leader of the component containing p_infoset.
1088+ // /
1089+ // / @param p_infoset The infoset to find the component leader for
1090+ // / @return Pointer to the leader infoset of the component
1091+ // /
1092+ // / Postcondition: Path from p_infoset to leader is compressed (flattened)
10861093 GameInfosetRep *FindSet (GameInfosetRep *p_infoset)
10871094 {
10881095 // Initialize/retrieve current parent
@@ -1100,7 +1107,14 @@ struct SubgameScratchData {
11001107 return leader;
11011108 }
11021109
1103- // DSU Union - merge current infoset into the start one and update the root candidate
1110+ // / DSU Union: Merges the components containing two infosets, updates the subgame root candidate.
1111+ // /
1112+ // / @param p_start_infoset The infoset whose component should absorb the other
1113+ // / @param p_current_infoset The infoset whose component is being merged
1114+ // / @param p_current_node The node being processed, considered as potential subgame root
1115+ // /
1116+ // / Postcondition: Both infosets belong to the same component
1117+ // / Postcondition: The component's subgame_root_candidate is updated to the highest node
11041118 void UnionSets (GameInfosetRep *p_start_infoset, GameInfosetRep *p_current_infoset,
11051119 GameNodeRep *p_current_node)
11061120 {
@@ -1122,16 +1136,37 @@ struct SubgameScratchData {
11221136 }
11231137};
11241138
1125- // Generate a single component starting from a given node
1139+ // / Generates a single connected component starting from a given node.
1140+ // /
1141+ // / The local frontier driving the exploration is a priority queue.
1142+ // / This design choice ensures that nodes are processed before their ancestors.
1143+ // /
1144+ // / Starting from p_start_node, explores the game tree by:
1145+ // / 1. Adding all members of each encountered infoset (horizontal expansion of the frontier)
1146+ // / 2. Moving to parent nodes (vertical expansion of the frontier)
1147+ // / 3. When hitting nodes from previously-generated components (external collision)
1148+ // / it merges the components and adds the root of the external component to the frontier
1149+ // /
1150+ // / The component gathers all infosets that share the same minimal subgame root.
1151+ // / The highest node processed that empties the frontier without adding any new nodes
1152+ // / to horizontal expansion becomes the subgame root candidate.
1153+ // /
1154+ // / @param p_data The DSU data structure to update with component information
1155+ // / @param p_start_node The node to start component generation from
1156+ // /
1157+ // / Precondition: p_start_node must be non-terminal
1158+ // / Precondition: p_start_node's infoset must not already be in p_data.dsu_parent
1159+ // / Postcondition: p_data.subgame_root_candidate[leader] contains the highest node
1160+ // / in the newly-formed component
11261161void GenerateComponent (SubgameScratchData &p_data, GameNodeRep *p_start_node)
11271162{
11281163 auto *start_infoset = p_start_node->GetInfoset ().get ();
1129- p_data.current_token ++;
11301164
1165+ std::unordered_set<GameNodeRep *> visited_this_component;
11311166 std::priority_queue<GameNodeRep *, std::vector<GameNodeRep *>, NodeCmp> local_frontier;
11321167
11331168 local_frontier.push (p_start_node);
1134- p_data. node_visited_token [p_start_node-> GetNumber ()] = p_data. current_token ;
1169+ visited_this_component. insert (p_start_node) ;
11351170
11361171 while (!local_frontier.empty ()) {
11371172 auto *curr = local_frontier.top ();
@@ -1145,9 +1180,9 @@ void GenerateComponent(SubgameScratchData &p_data, GameNodeRep *p_start_node)
11451180 if (is_external_collision) {
11461181 // We hit a node belonging to a previously generated component.
11471182 auto *candidate_root = p_data.subgame_root_candidate .at (p_data.FindSet (curr_infoset));
1148- if (p_data. node_visited_token [candidate_root-> GetNumber ()] != p_data. current_token ) {
1183+ if (!visited_this_component. count (candidate_root) ) {
11491184 local_frontier.push (candidate_root);
1150- p_data. node_visited_token [candidate_root-> GetNumber ()] = p_data. current_token ;
1185+ visited_this_component. insert (candidate_root) ;
11511186 }
11521187 }
11531188 else {
@@ -1158,35 +1193,44 @@ void GenerateComponent(SubgameScratchData &p_data, GameNodeRep *p_start_node)
11581193 continue ;
11591194 }
11601195 local_frontier.push (member);
1161- p_data. node_visited_token [member-> GetNumber ()] = p_data. current_token ;
1196+ visited_this_component. insert (member) ;
11621197 }
11631198 }
11641199
11651200 if (!local_frontier.empty ()) {
11661201 if (auto parent_sp = curr->GetParent ()) {
11671202 auto *parent = parent_sp.get ();
1168- if (p_data. node_visited_token [parent-> GetNumber ()] != p_data. current_token ) {
1203+ if (!visited_this_component. count (parent) ) {
11691204 local_frontier.push (parent);
1170- p_data. node_visited_token [parent-> GetNumber ()] = p_data. current_token ;
1205+ visited_this_component. insert (parent) ;
11711206 }
11721207 }
11731208 }
11741209 }
11751210}
11761211
1177- // For each infoset, find the root of the smallest subgame containing it
1212+ // / For each infoset in the game, computes the root of the smallest subgame containing it.
1213+ // /
1214+ // / Processes nodes in reverse DFS order (postorder), building a component for each node,
1215+ // / skipping a node if it is:
1216+ // / 1. A member of an infoset already belonging to some component, or
1217+ // / 2. Terminal
1218+ // /
1219+ // / @param p_game The game tree
1220+ // / @return Map from each infoset to the root of its smallest containing subgame
1221+ // /
1222+ // / Precondition: p_game must be a valid game tree
1223+ // / Postcondition: Every infoset in the game is mapped to exactly one subgame root node
1224+ // / Returns: Empty map if the game root is terminal (trivial game)
11781225std::map<GameInfosetRep *, GameNodeRep *> FindSubgameRoots (const Game &p_game)
11791226{
11801227 if (p_game->GetRoot ()->IsTerminal ()) {
11811228 return {};
11821229 }
11831230
11841231 SubgameScratchData data;
1185- data.current_token ++;
11861232 const int num_nodes = p_game->NumNodes ();
11871233
1188- data.node_visited_token .resize (num_nodes + 1 , 0 );
1189-
11901234 // Collect nodes in ascending order (depth-first traversal) -- TODO: Replace with proper iterator
11911235 std::vector<GameNodeRep *> nodes_ascending;
11921236 nodes_ascending.reserve (num_nodes);
0 commit comments