use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Loss,
Win,
Draw,
}
pub fn outcomes(succ: &[Vec<usize>]) -> Vec<Outcome> {
let n = succ.len();
let mut pred = vec![Vec::new(); n];
for (u, outs) in succ.iter().enumerate() {
for &v in outs {
pred[v].push(u);
}
}
let mut remaining: Vec<usize> = succ.iter().map(|o| o.len()).collect();
let mut label: Vec<Option<Outcome>> = vec![None; n];
let mut queue: VecDeque<usize> = VecDeque::new();
for v in 0..n {
if succ[v].is_empty() {
label[v] = Some(Outcome::Loss);
queue.push_back(v);
}
}
while let Some(v) = queue.pop_front() {
let lv = label[v].unwrap();
for &u in &pred[v] {
if label[u].is_some() {
continue;
}
match lv {
Outcome::Loss => {
label[u] = Some(Outcome::Win);
queue.push_back(u);
}
Outcome::Win => {
remaining[u] -= 1;
if remaining[u] == 0 {
label[u] = Some(Outcome::Loss);
queue.push_back(u);
}
}
Outcome::Draw => {}
}
}
}
label
.into_iter()
.map(|l| l.unwrap_or(Outcome::Draw))
.collect()
}
pub fn p_positions(succ: &[Vec<usize>]) -> Vec<usize> {
outcomes(succ)
.into_iter()
.enumerate()
.filter(|(_, o)| *o == Outcome::Loss)
.map(|(i, _)| i)
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScoreInterval {
pub left: i128,
pub right: i128,
}
fn score_dfs(
succ: &[Vec<usize>],
terminal_score: &[i128],
v: usize,
state: &mut [u128],
memo: &mut [Option<ScoreInterval>],
) -> Option<ScoreInterval> {
match state[v] {
2 => return memo[v],
1 => return None, _ => {}
}
state[v] = 1;
let result = if succ[v].is_empty() {
ScoreInterval {
left: terminal_score[v],
right: terminal_score[v],
}
} else {
let mut best_left = i128::MIN;
let mut best_right = i128::MAX;
for &w in &succ[v] {
let cw = score_dfs(succ, terminal_score, w, state, memo)?;
best_left = best_left.max(cw.right);
best_right = best_right.min(cw.left);
}
ScoreInterval {
left: best_left,
right: best_right,
}
};
state[v] = 2;
memo[v] = Some(result);
Some(result)
}
pub fn scoring_values(succ: &[Vec<usize>], terminal_score: &[i128]) -> Option<Vec<ScoreInterval>> {
let n = succ.len();
assert_eq!(n, terminal_score.len(), "one score per position");
let mut state = vec![0u128; n];
let mut memo: Vec<Option<ScoreInterval>> = vec![None; n];
for v in 0..n {
score_dfs(succ, terminal_score, v, &mut state, &mut memo)?;
}
Some(memo.into_iter().map(|m| m.unwrap()).collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_edge_and_terminal() {
let succ = vec![vec![1], vec![]];
assert_eq!(outcomes(&succ), vec![Outcome::Win, Outcome::Loss]);
}
#[test]
fn two_cycle_is_a_draw() {
let succ = vec![vec![1], vec![0]];
assert_eq!(outcomes(&succ), vec![Outcome::Draw, Outcome::Draw]);
}
#[test]
fn nim_heap_as_a_path_matches_normal_play() {
let n = 6;
let succ: Vec<Vec<usize>> = (0..=n).map(|h| (0..h).collect()).collect();
let out = outcomes(&succ);
assert_eq!(out[0], Outcome::Loss);
assert!((1..=n).all(|h| out[h] == Outcome::Win));
}
#[test]
fn cycle_with_exit_resolves() {
let succ = vec![vec![1], vec![2, 0], vec![]];
assert_eq!(
outcomes(&succ),
vec![Outcome::Loss, Outcome::Win, Outcome::Loss]
);
}
#[test]
fn scoring_terminal_and_single_move() {
let succ = vec![vec![1], vec![]];
let v = scoring_values(&succ, &[0, 5]).unwrap();
assert_eq!(v[1], ScoreInterval { left: 5, right: 5 });
assert_eq!(v[0], ScoreInterval { left: 5, right: 5 });
}
#[test]
fn scoring_first_move_advantage() {
let succ = vec![vec![1, 2], vec![], vec![]];
let v = scoring_values(&succ, &[0, 3, 7]).unwrap();
assert_eq!(v[0], ScoreInterval { left: 7, right: 3 });
}
#[test]
fn scoring_alternation_through_a_forced_move() {
let succ = vec![vec![1], vec![2, 3], vec![], vec![]];
let v = scoring_values(&succ, &[0, 0, 1, 4]).unwrap();
assert_eq!(v[1], ScoreInterval { left: 4, right: 1 });
assert_eq!(v[0], ScoreInterval { left: 1, right: 4 });
}
#[test]
fn scoring_is_none_on_a_cycle() {
let succ = vec![vec![1], vec![0]];
assert_eq!(scoring_values(&succ, &[0, 0]), None);
}
}