Skip to content

Commit df5d91a

Browse files
committed
Replace token-based visited tracking with std::unordered_set; correct edge case of the empty game; add documentation
1 parent f88d276 commit df5d91a

File tree

1 file changed

+64
-20
lines changed

1 file changed

+64
-20
lines changed

src/games/gametree.cc

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ bool GameNodeRep::IsSuccessorOf(GameNode p_node) const
374374
bool 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()
10661066
namespace { // 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
10691071
struct 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
10771078
struct 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
11261161
void 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)
11781225
std::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

Comments
 (0)