1
//! Authors: Maurice Laveaux
2
//!
3
//! Implementation of the priority promotion algorithm introduced in:
4
//!
5
//! > Massimo Benerecetti, Daniele Dell'Erba, and Fabio Mogavero. Solving
6
//! > Parity Games via Priority Promotion, pages 270-290. Springer International
7
//! > Publishing, Cham, 2016.
8
//!
9
//! Given the current triple (region_function, strategy, prio), referred to as
10
//! state, a region R is extracted by means of the query function. An alpha-region
11
//! is a set of vertices R and a witness strategy sigma. So that for all plays
12
//! consistent with sigma, they either stay within R and are winning for alpha.
13
//! Or they escape R via vertices having the highest priority in the game. The
14
//! initial state is (priority_function, empty_strategy, min(range(priority_function)))
15
//!
16
//! If the region R is a dominion in the whole game, the dominion is removed from
17
//! the game and the algorithm runs on the remaining game. If the extracted region
18
//! is open in the subgame G >= prio. Which is the subgame with only vertices
19
//! having greater or equal priority then prio. The next state becomes
20
//! (region_function\[R -> prio\], strategy\*, min(range(region_function >= prio))).
21
//!
22
//! If the alpha-region is a dominion in the subgame G >= prio, the lowest priority
23
//! region that the opponent can flee to is determined in [`PriorityPromotionSolver::promote_sub_dominion`].
24
//! And then the next state becomes (region_function\*\[R -> prio\*\], strategy\*, prio\*), and
25
//! in region_function\* all regions below prio\* are reset to the original priority.
26
//!
27
//! The strategy\* was not presented in the paper, but was partially determined
28
//! by the follow up paper Improving Priority Promotion for parity games. For
29
//! the attractor for some region R the strategy is determined by the witness
30
//! that alpha can reach R. For vertices inside the region R a witness core strategy
31
//! sigma is given. For all vertices inside R where no strategy is defined yet
32
//! an arbitrary successor inside R is taken to complete the strategy.
33

            
34
use std::collections::VecDeque;
35

            
36
use bitvec::bitvec;
37
use bitvec::order::Lsb0;
38
use log::debug;
39
use log::trace;
40

            
41
use crate::PG;
42
use crate::Player;
43
use crate::Pred;
44
use crate::Predecessors;
45
use crate::Priority;
46
use crate::Strat;
47
use crate::Strategy;
48
use crate::VertexIndex;
49
use crate::zielonka::Set;
50

            
51
/// Solves the given parity game using the priority promotion algorithm.
52
///
53
/// Returns the winning sets for both players and optionally their strategies.
54
200
pub fn solve_priority_promotion<G: PG>(game: &G, compute_strategy: bool) -> ([Set; 2], Option<[Strategy; 2]>) {
55
200
    if compute_strategy {
56
100
        let (solution, strategy) = solve_priority_promotion_impl::<G, Strategy>(game);
57
100
        (solution, Some(strategy))
58
    } else {
59
100
        let (solution, _) = solve_priority_promotion_impl::<G, ()>(game);
60
100
        (solution, None)
61
    }
62
200
}
63

            
64
/// Solves the given parity game using the priority promotion algorithm,
65
/// computing a strategy representation of type `S`.
66
200
fn solve_priority_promotion_impl<G: PG, S: Strat>(game: &G) -> ([Set; 2], [S; 2]) {
67
200
    debug_assert!(
68
200
        game.is_total(),
69
        "Priority promotion solver requires a total parity game"
70
    );
71

            
72
200
    let mut solver = PriorityPromotionSolver::new(game);
73
200
    let strategy = solver.solve::<S>();
74

            
75
200
    let mut w0 = bitvec![usize, Lsb0; 0; game.num_of_vertices()];
76
200
    let mut w1 = bitvec![usize, Lsb0; 0; game.num_of_vertices()];
77
200
    let mut s0 = S::new();
78
200
    let mut s1 = S::new();
79

            
80
15000
    for v in game.iter_vertices() {
81
15000
        let prio = solver.region_function[*v];
82
15000
        debug_assert!(prio.is_none(), "All vertices should be solved");
83

            
84
15000
        let winner = solver.final_winner[*v];
85
15000
        match winner {
86
            Player::Even => {
87
7737
                w0.set(*v, true);
88
7737
                if game.owner(v) == Player::Even
89
3186
                    && let Some(target) = strategy.get(v)
90
2126
                {
91
2126
                    s0.set(v, target);
92
5611
                }
93
            }
94
            Player::Odd => {
95
7263
                w1.set(*v, true);
96
7263
                if game.owner(v) == Player::Odd
97
2947
                    && let Some(target) = strategy.get(v)
98
1973
                {
99
1973
                    s1.set(v, target);
100
5290
                }
101
            }
102
        }
103
    }
104

            
105
200
    ([w0, w1], [s0, s1])
106
200
}
107

            
108
/// Internal solver state for the priority promotion algorithm.
109
struct PriorityPromotionSolver<'a, G: PG> {
110
    game: &'a G,
111

            
112
    /// Precomputed predecessors for backward iteration.
113
    predecessors: Predecessors<'a>,
114

            
115
    /// Maps each vertex to its current region priority.
116
    /// `None` indicates a solved vertex.
117
    region_function: Vec<Option<Priority>>,
118

            
119
    /// Stores a list of vertices not yet solved by the algorithm.
120
    unsolved: Vec<VertexIndex>,
121

            
122
    /// Count the number of vertices per region, to speed up [`Self::next_priority`].
123
    regions: Vec<usize>,
124

            
125
    /// This is a reused queue with vertices to compute the attractor set from.
126
    todo: VecDeque<VertexIndex>,
127

            
128
    /// The number of promotions required.
129
    promotions: usize,
130

            
131
    /// The number of dominions found.
132
    dominions: usize,
133

            
134
    /// Records the winning player for each vertex (set when a dominion is found).
135
    final_winner: Vec<Player>,
136
}
137

            
138
impl<'a, G: PG> PriorityPromotionSolver<'a, G> {
139
    /// Creates a new priority promotion solver for the given parity game.
140
200
    fn new(game: &'a G) -> Self {
141
200
        let num_vertices = game.num_of_vertices();
142

            
143
        // The lowest priority in the game (the highest number).
144
200
        let mut lowest_region = Priority::new(0);
145

            
146
        // Set region_function to the original priorities and initialize the mapping.
147
200
        let mut region_function = vec![None; num_vertices];
148
200
        let mut unsolved = Vec::with_capacity(num_vertices);
149

            
150
15000
        for v in game.iter_vertices() {
151
15000
            let prio = game.priority(v);
152
15000
            region_function[*v] = Some(prio);
153
15000
            unsolved.push(v);
154
15000
            lowest_region = lowest_region.max(prio);
155
15000
        }
156

            
157
        // Initialize all regions that have some vertices.
158
200
        let mut regions = vec![0usize; lowest_region.value() + 1];
159
15000
        for &r in &region_function {
160
15000
            if let Some(prio) = r {
161
15000
                regions[prio.value()] += 1;
162
15000
            }
163
        }
164

            
165
200
        PriorityPromotionSolver {
166
200
            game,
167
200
            predecessors: Predecessors::new(game),
168
200
            region_function,
169
200
            unsolved,
170
200
            regions,
171
200
            todo: VecDeque::new(),
172
200
            promotions: 0,
173
200
            dominions: 0,
174
200
            final_winner: vec![Player::Even; num_vertices],
175
200
        }
176
200
    }
177

            
178
    /// Compute winning strategies by means of priority promotion, follows the
179
    /// paper as closely as possible.
180
    ///
181
    /// # Details
182
    ///
183
    /// Important note: instead of actually repeatedly removing dominions from
184
    /// the game, the game is kept the same but the region_function is used to
185
    /// determine which vertices still are not solved. This is done because
186
    /// removing subgames allocates new memory repeatedly and parity games
187
    /// can be huge.
188
200
    fn solve<S: Strat>(&mut self) -> S {
189
200
        let mut strategy = S::new();
190

            
191
        // Find the highest priority in the game.
192
200
        let mut prio = self.next_priority(Priority::new(self.regions.len() - 1));
193

            
194
        // The algorithm was tail recursive so can also be written as iteration.
195
        loop {
196
1989
            self.query(&mut strategy, prio);
197

            
198
1989
            if self.is_open(prio, true) {
199
1327
                debug!("Newly computed region is open in the subgame, with p = {}", prio);
200
1327
                self.print_region(prio);
201

            
202
                // Keep the new region_function and substrategy, but go to the next priority.
203
1327
                prio = self.next_priority(Priority::new(prio.value().saturating_sub(1)));
204
662
            } else if !self.is_open(prio, false) {
205
                // This is a dominion D in the whole game, compute the attractor
206
                // for this region.
207
514
                debug_assert!(self.todo.is_empty());
208

            
209
23972
                for &v in &self.unsolved {
210
23972
                    if self.region_function[*v] == Some(prio) {
211
8641
                        self.todo.push_back(v);
212
15331
                    }
213
                }
214

            
215
514
                self.compute_attractor(&mut strategy, prio, false);
216

            
217
                // Remove the dominion from the game and keep the unsolved vertices, also reset
218
                // lower priorities and set region of prio to the COMPUTED_REGION.
219
514
                debug!("Found the dominion D, with p = {}", prio);
220
514
                self.print_region(prio);
221

            
222
                // Record the winner for this dominion.
223
514
                let winner = Player::from_priority(&prio);
224
38300
                for v in self.game.iter_vertices() {
225
38300
                    if self.region_function[*v] == Some(prio) {
226
15000
                        self.final_winner[*v] = winner;
227
23300
                    }
228
                }
229

            
230
                // Reset the unsolved set and remove all regions, also add one dominion to statistics.
231
514
                self.unsolved.clear();
232
514
                self.regions.fill(0);
233
514
                self.dominions += 1;
234

            
235
38300
                for v in self.game.iter_vertices() {
236
38300
                    if self.region_function[*v] == Some(prio) {
237
15000
                        // Assign a special region indicating that it's solved.
238
15000
                        self.region_function[*v] = None;
239
23300
                    } else if self.region_function[*v].is_some() {
240
8972
                        let original_prio = self.game.priority(v);
241
8972
                        self.region_function[*v] = Some(original_prio);
242
8972
                        strategy.remove(v);
243
8972

            
244
8972
                        // Add the not solved vertices to the unsolved set and add vertices to their region.
245
8972
                        self.unsolved.push(v);
246
8972
                        self.regions[original_prio.value()] += 1;
247
14328
                    }
248
                }
249

            
250
514
                if self.unsolved.is_empty() {
251
200
                    break; // Stop the algorithm, as all the vertices were solved.
252
314
                }
253

            
254
                // Reset the game and find the highest priority in the game.
255
314
                prio = self.next_priority(Priority::new(self.regions.len() - 1));
256
            } else {
257
                // The game is a dominion, but only in the subgame, so promote its priority.
258
148
                debug!("Promoted dominion D, with p = {}", prio);
259
148
                prio = self.promote_sub_dominion(&mut strategy, prio);
260
148
                debug!(" to {}", prio);
261
148
                self.print_region(prio);
262
            }
263
        }
264

            
265
200
        debug!(
266
            "{} dominions found, and {} promotions required",
267
            self.dominions, self.promotions
268
        );
269

            
270
200
        strategy
271
200
    }
272

            
273
    /// From the state (region_function, strategy, prio) compute the new alpha-region
274
    /// R and update region_function\[R -> p\]. The strategy will be updated in
275
    /// [`Self::compute_attractor`]. The unsolved set is used to quickly iterate unsolved vertices.
276
    /// The todo queue is passed to be reused by [`Self::compute_attractor`].
277
1989
    fn query<S: Strat>(&mut self, strategy: &mut S, prio: Priority) {
278
        // Make sure nothing else is stored in the todo.
279
1989
        debug_assert!(self.todo.is_empty());
280

            
281
        // R* = region_function^-1(prio), this results in the todo for the attractor
282
        // computation, the initial set essentially.
283
120439
        for &v in &self.unsolved {
284
120439
            if self.region_function[*v] == Some(prio) {
285
23098
                self.todo.push_back(v);
286
97341
            }
287
        }
288

            
289
        // (region_function[R -> prio], strategy*) <- computeAttractor_G(todo, strategy
290
        // restricted to todo)
291
1989
        self.compute_attractor(strategy, prio, true);
292
1989
    }
293

            
294
    /// Compute the attractor set A for vertices in todo, with alpha being prio mod 2.
295
    /// `in_subgraph` indicates that only vertices in game <= prio are considered. This
296
    /// updates region_function\[A -> prio\]. The strategy is changed for alpha for the
297
    /// attraction witness. The remaining vertices of alpha without a strategy can pick
298
    /// any vertex inside A as witness.
299
2503
    fn compute_attractor<S: Strat>(&mut self, strategy: &mut S, prio: Priority, in_subgraph: bool) {
300
2503
        let alpha = Player::from_priority(&prio);
301

            
302
        // O(V): Compute the attractor set to the alpha-region.
303
48709
        while let Some(w) = self.todo.pop_front() {
304
            // Check all predecessors v of w.
305
61647
            for v in self.predecessors.predecessors(w) {
306
                // Skip predecessors that are already in the attractor set, also skip
307
                // vertices outside the subgame G <= prio. Or vertices that are computed.
308
61647
                if self.region_function[*v] == Some(prio)
309
30863
                    || self.region_function[*v].is_none()
310
28089
                    || (in_subgraph && self.region_function[*v].is_some_and(|region| region > prio))
311
                {
312
43470
                    continue;
313
18177
                }
314

            
315
18177
                if self.game.owner(v) == alpha {
316
8939
                    // sigma(v) = w, a valid strategy for alpha is to pick a successor in A.
317
8939
                    strategy.set(v, w);
318
8939
                } else {
319
                    // Check if all successors (v, x) subset A, thus if they end up in a vertex with prio.
320
12986
                    let is_subset = self.game.outgoing_edges(v).all(|edge| {
321
12986
                        let x = edge.to();
322

            
323
                        // Skip vertices that are not considered in the subgraph G <= prio
324
                        // or that already belong to COMPUTED_REGION.
325
12986
                        if self.region_function[*x] == Some(prio) || self.region_function[*x].is_none() {
326
9100
                            return true;
327
3886
                        }
328

            
329
                        // Either only take vertices in G <= prio or all when in_subgraph is false.
330
3886
                        if self.region_function[*x].is_some_and(|region| region < prio) || !in_subgraph {
331
3710
                            return false;
332
176
                        }
333

            
334
176
                        true
335
12986
                    });
336

            
337
9238
                    if !is_subset {
338
3710
                        continue; // not in the attractor set yet!
339
5528
                    }
340

            
341
                    // For opponent controlled vertices no strategy exists, so
342
                    // every possible outgoing edge is losing.
343
                }
344

            
345
                // Add a vertex to their new region and remove from the old one.
346
14467
                let old_region = self.region_function[*v].expect("Unsolved vertices must have a region");
347
14467
                self.regions[old_region.value()] -= 1;
348
14467
                self.regions[prio.value()] += 1;
349

            
350
                // When this part is reached, all liberties of v are gone or v belongs
351
                // to alpha, so add vertex v to the attractor set.
352
14467
                self.region_function[*v] = Some(prio);
353
14467
                self.todo.push_back(v);
354
            }
355
        }
356

            
357
        // R \ domain(tau restricted to R*), essentially vertices in R belonging to
358
        // alpha where no strategy is defined yet. These can pick an arbitrary
359
        // successor that can reach R \ R*, these already have an attraction
360
        // strategy so that is always fine.
361
144411
        for &v in &self.unsolved {
362
144411
            if self.region_function[*v] == Some(prio) && self.game.owner(v) == alpha && strategy.get(v).is_none() {
363
12174
                for edge in self.game.outgoing_edges(v) {
364
12174
                    let w = edge.to();
365

            
366
12174
                    if self.region_function[*w] == Some(prio) {
367
                        // There exists some (v, w) in E such that w belongs to R (has r[w] == prio).
368
6729
                        strategy.set(v, w);
369
6729
                        break;
370
5445
                    }
371
                }
372
134913
            }
373
        }
374
2503
    }
375

            
376
    /// Determine whether the alpha-region with priority `prio` is open in G, or
377
    /// in G <= prio (indicated by `in_subgraph`). This means that for all vertices
378
    /// v with region_function\[v\] equal to prio, this is set R. When v belongs
379
    /// to alpha, determined by prio mod 2, there is some witness successor in R.
380
    /// For opponent vertices all successors lead to R, no witness to escape basically.
381
2651
    fn is_open(&self, prio: Priority, in_subgraph: bool) -> bool {
382
2651
        let alpha = Player::from_priority(&prio);
383

            
384
        // O(V): Loop over unsolved vertices and find vertices belonging to region with prio.
385
80852
        for &v in &self.unsolved {
386
80852
            if self.region_function[*v] == Some(prio) {
387
26089
                if self.game.owner(v) != alpha {
388
                    // For all (v, u) in E, u should belong to R.
389
21174
                    for edge in self.game.outgoing_edges(v) {
390
21174
                        let u = edge.to();
391

            
392
                        // There is an edge from opponent to a vertex in the subgraph or in the whole graph.
393
21174
                        if self.region_function[*u].is_some()
394
20850
                            && ((in_subgraph && self.region_function[*u].is_some_and(|region| region < prio))
395
20172
                                || (!in_subgraph && self.region_function[*u] != Some(prio)))
396
                        {
397
826
                            return true;
398
20348
                        }
399
                    }
400
                } else {
401
                    // If there exists a (v, u) to R its closed.
402
6752
                    let is_open = self
403
6752
                        .game
404
6752
                        .outgoing_edges(v)
405
8378
                        .all(|edge| self.region_function[*edge.to()] != Some(prio));
406

            
407
6752
                    if is_open {
408
649
                        return true;
409
6103
                    }
410
                }
411
54763
            }
412
        }
413

            
414
1176
        false
415
2651
    }
416

            
417
    /// Promote a sub dominion D to the minimum region greater than prio that the
418
    /// opponent can reach. This updates region_function\[D -> prio\*\] and resets
419
    /// all priorities greater than prio\* to the original priority function. The
420
    /// strategy is updated by means of [`Self::compute_attractor`]. And lower strategies
421
    /// are set to `None` (no strategy known).
422
    ///
423
    /// # Details
424
    ///
425
    /// This is referred to as r\* = bep(R, r) in the paper (best escape priority).
426
    /// For every opponent vertex this is the highest priority that it can reach.
427
    /// The region it can reach belongs to alpha,
428
    /// otherwise it would be attracted in some earlier state.
429
148
    fn promote_sub_dominion<S: Strat>(&mut self, strategy: &mut S, prio: Priority) -> Priority {
430
148
        let alpha = Player::from_priority(&prio);
431

            
432
        // O(V): It is only a dominion in the subgraph, determine lowest p > prio
433
        // that opponent can escape to.
434
148
        let mut promotion = Priority::new(self.regions.len() - 1);
435

            
436
9721
        for &v in &self.unsolved {
437
9721
            if self.region_function[*v] == Some(prio) && self.game.owner(v) != alpha {
438
                // For all (v, u) in E collect the lowest priority larger than prio that opponent can flee to.
439
2875
                for edge in self.game.outgoing_edges(v) {
440
2875
                    let u = edge.to();
441

            
442
2875
                    if let Some(region) = self.region_function[*u]
443
2874
                        && region > prio
444
211
                    {
445
211
                        promotion = promotion.min(region);
446
2664
                    }
447
                }
448
7117
            }
449
        }
450

            
451
148
        self.promotions += 1;
452

            
453
        // Here the prio region is promoted to the new priority and all lower positions
454
        // are reset.
455
9721
        for &v in &self.unsolved {
456
9721
            if self.region_function[*v] == Some(prio) {
457
3018
                // Promote the current region to the promotion priority.
458
3018
                self.regions[prio.value()] -= 1;
459
3018
                self.region_function[*v] = Some(promotion);
460
3018
                self.regions[promotion.value()] += 1;
461
6703
            } else if self.region_function[*v].is_some_and(|region| region < promotion) {
462
2519
                // Reset all vertices higher to the original priorities, remove the strategy.
463
2519
                let old_region = self.region_function[*v].expect("Unsolved vertices must have a region");
464
2519
                self.regions[old_region.value()] -= 1;
465
2519
                let original_prio = self.game.priority(v);
466
2519
                self.region_function[*v] = Some(original_prio);
467
2519
                strategy.remove(v);
468
2519
                self.regions[original_prio.value()] += 1;
469
4184
            }
470
        }
471

            
472
148
        promotion
473
148
    }
474

            
475
    /// Print the vertices with region_function\[v\] equal to prio, representing the region.
476
    ///
477
    /// This costs O(V) so only enable this in debug.
478
1989
    fn print_region(&self, prio: Priority) {
479
1989
        if log::log_enabled!(log::Level::Trace) {
480
            let vertices: Vec<_> = self
481
                .unsolved
482
                .iter()
483
                .filter(|&&v| self.region_function[*v] == Some(prio))
484
                .map(|v| v.value())
485
                .collect();
486
            trace!(
487
                "alpha-region[{}] = {{ {} }}",
488
                prio,
489
                vertices.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(",")
490
            );
491
1989
        }
492
1989
    }
493

            
494
    /// Computes max(rng(region_function <= prio)), so the next lower priority,
495
    /// smaller or equal to prio, that some vertex has.
496
    ///
497
    /// Starting from the current priority, find the next region that exists.
498
    /// This should never go out of bounds as the lowest region will always be a dominion.
499
1841
    fn next_priority(&self, prio: Priority) -> Priority {
500
1841
        let mut p = prio.value();
501
1981
        while self.regions[p] == 0 {
502
140
            assert!(p > 0, "next_priority went out of bounds");
503
140
            p -= 1;
504
        }
505
1841
        Priority::new(p)
506
1841
    }
507
}
508

            
509
#[cfg(test)]
510
mod tests {
511
    use merc_io::DumpFiles;
512
    use merc_utilities::random_test;
513

            
514
    use crate::random_parity_game;
515
    use crate::solve_zielonka;
516
    use crate::verify_solution;
517
    use crate::write_pg;
518

            
519
    use super::*;
520

            
521
    #[test]
522
    #[cfg_attr(miri, ignore)] // Miri is too slow for this test.
523
1
    fn test_random_priority_promotion_solver() {
524
100
        random_test(100, |rng| {
525
100
            let mut files = DumpFiles::new("test_random_priority_promotion_solver");
526
100
            let game = random_parity_game(rng, true, 100, 5, 3);
527

            
528
100
            files.dump("input.pg", |writer| write_pg(writer, &game)).unwrap();
529

            
530
100
            let (solution, strategy) = solve_priority_promotion(&game, true);
531
100
            verify_solution(&game, &solution, &strategy.unwrap());
532
100
        });
533
1
    }
534

            
535
    #[test]
536
    #[cfg_attr(miri, ignore)]
537
1
    fn test_priority_promotion_matches_zielonka() {
538
100
        random_test(100, |rng| {
539
100
            let game = random_parity_game(rng, true, 50, 4, 3);
540

            
541
100
            let (pp_solution, _) = solve_priority_promotion(&game, false);
542
100
            let (zielonka_solution, _) = solve_zielonka(&game, false);
543

            
544
100
            assert_eq!(
545
100
                pp_solution[0], zielonka_solution[0],
546
                "Even winning sets differ between priority promotion and Zielonka"
547
            );
548
100
            assert_eq!(
549
100
                pp_solution[1], zielonka_solution[1],
550
                "Odd winning sets differ between priority promotion and Zielonka"
551
            );
552
100
        });
553
1
    }
554
}