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
7384
                w0.set(*v, true);
88
7384
                if game.owner(v) == Player::Even
89
3013
                    && let Some(target) = strategy.get(v)
90
2020
                {
91
2020
                    s0.set(v, target);
92
5364
                }
93
            }
94
            Player::Odd => {
95
7616
                w1.set(*v, true);
96
7616
                if game.owner(v) == Player::Odd
97
3189
                    && let Some(target) = strategy.get(v)
98
2109
                {
99
2109
                    s1.set(v, target);
100
5507
                }
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
    /// Reused per-vertex counter, used during attractor computation to count the
129
    /// number of successors of an opponent vertex that can still enter the
130
    /// region being computed.
131
    attractor_counters: Vec<usize>,
132

            
133
    /// The number of promotions required.
134
    promotions: usize,
135

            
136
    /// The number of dominions found.
137
    dominions: usize,
138

            
139
    /// Records the winning player for each vertex (set when a dominion is found).
140
    final_winner: Vec<Player>,
141
}
142

            
143
impl<'a, G: PG> PriorityPromotionSolver<'a, G> {
144
    /// Creates a new priority promotion solver for the given parity game.
145
200
    fn new(game: &'a G) -> Self {
146
200
        let num_vertices = game.num_of_vertices();
147

            
148
        // The lowest priority in the game (the highest number).
149
200
        let mut lowest_region = Priority::new(0);
150

            
151
        // Set region_function to the original priorities and initialize the mapping.
152
200
        let mut region_function = vec![None; num_vertices];
153
200
        let mut unsolved = Vec::with_capacity(num_vertices);
154

            
155
15000
        for v in game.iter_vertices() {
156
15000
            let prio = game.priority(v);
157
15000
            region_function[*v] = Some(prio);
158
15000
            unsolved.push(v);
159
15000
            lowest_region = lowest_region.max(prio);
160
15000
        }
161

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

            
170
200
        PriorityPromotionSolver {
171
200
            game,
172
200
            predecessors: Predecessors::new(game),
173
200
            region_function,
174
200
            unsolved,
175
200
            regions,
176
200
            todo: VecDeque::new(),
177
200
            attractor_counters: vec![0; num_vertices],
178
200
            promotions: 0,
179
200
            dominions: 0,
180
200
            final_winner: vec![Player::Even; num_vertices],
181
200
        }
182
200
    }
183

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

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

            
200
        // The algorithm was tail recursive so can also be written as iteration.
201
        loop {
202
2004
            self.query(&mut strategy, prio);
203

            
204
2004
            if self.is_open(prio, true) {
205
1348
                debug!("Newly computed region is open in the subgame, with p = {}", prio);
206
1348
                self.print_region(prio);
207

            
208
                // Keep the new region_function and substrategy, but go to the next priority.
209
1348
                prio = self.next_priority(Priority::new(prio.value().saturating_sub(1)));
210
656
            } else if !self.is_open(prio, false) {
211
                // This is a dominion D in the whole game, compute the attractor
212
                // for this region.
213
499
                debug_assert!(self.todo.is_empty());
214

            
215
23765
                for &v in &self.unsolved {
216
23765
                    if self.region_function[*v] == Some(prio) {
217
8646
                        self.todo.push_back(v);
218
15119
                    }
219
                }
220

            
221
499
                self.compute_attractor(&mut strategy, prio, false);
222

            
223
                // Remove the dominion from the game and keep the unsolved vertices, also reset
224
                // lower priorities and set region of prio to the COMPUTED_REGION.
225
499
                debug!("Found the dominion D, with p = {}", prio);
226
499
                self.print_region(prio);
227

            
228
                // Record the winner for this dominion.
229
499
                let winner = Player::from_priority(prio);
230
37350
                for v in self.game.iter_vertices() {
231
37350
                    if self.region_function[*v] == Some(prio) {
232
15000
                        self.final_winner[*v] = winner;
233
22350
                    }
234
                }
235

            
236
                // Reset the unsolved set and remove all regions, also add one dominion to statistics.
237
499
                self.unsolved.clear();
238
499
                self.regions.fill(0);
239
499
                self.dominions += 1;
240

            
241
37350
                for v in self.game.iter_vertices() {
242
37350
                    if self.region_function[*v] == Some(prio) {
243
15000
                        // Assign a special region indicating that it's solved.
244
15000
                        self.region_function[*v] = None;
245
22350
                    } else if self.region_function[*v].is_some() {
246
8765
                        let original_prio = self.game.priority(v);
247
8765
                        self.region_function[*v] = Some(original_prio);
248
8765
                        strategy.remove(v);
249
8765

            
250
8765
                        // Add the not solved vertices to the unsolved set and add vertices to their region.
251
8765
                        self.unsolved.push(v);
252
8765
                        self.regions[original_prio.value()] += 1;
253
13585
                    }
254
                }
255

            
256
499
                if self.unsolved.is_empty() {
257
200
                    break; // Stop the algorithm, as all the vertices were solved.
258
299
                }
259

            
260
                // Reset the game and find the highest priority in the game.
261
299
                prio = self.next_priority(Priority::new(self.regions.len() - 1));
262
            } else {
263
                // The game is a dominion, but only in the subgame, so promote its priority.
264
157
                debug!("Promoted dominion D, with p = {}", prio);
265
157
                prio = self.promote_sub_dominion(&mut strategy, prio);
266
157
                debug!(" to {}", prio);
267
157
                self.print_region(prio);
268
            }
269
        }
270

            
271
200
        debug!(
272
            "{} dominions found, and {} promotions required",
273
            self.dominions, self.promotions
274
        );
275

            
276
200
        strategy
277
200
    }
278

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

            
287
        // R* = region_function^-1(prio), this results in the todo for the attractor
288
        // computation, the initial set essentially.
289
123262
        for &v in &self.unsolved {
290
123262
            if self.region_function[*v] == Some(prio) {
291
24033
                self.todo.push_back(v);
292
99229
            }
293
        }
294

            
295
        // (region_function[R -> prio], strategy*) <- computeAttractor_G(todo, strategy
296
        // restricted to todo)
297
2004
        self.compute_attractor(strategy, prio, true);
298
2004
    }
299

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

            
308
        // Initialise, for every opponent vertex still under consideration, the
309
        // number of its outgoing edges that lead to a vertex which can still
310
        // enter the region with priority `prio` (a "poppable" target: not yet
311
        // solved, and inside the subgame when `in_subgraph`). The opponent
312
        // vertex is attracted once all of those targets have been attracted.
313
147027
        for i in 0..self.unsolved.len() {
314
147027
            let v = self.unsolved[i];
315
147027
            if self.game.owner(v) != alpha {
316
75044
                self.attractor_counters[*v] = self
317
75044
                    .game
318
75044
                    .outgoing_edges(v)
319
98895
                    .filter(|edge| {
320
98895
                        let x = edge.to();
321
98895
                        self.region_function[*x].is_some()
322
96848
                            && !(in_subgraph && self.region_function[*x].is_some_and(|region| region > prio))
323
98895
                    })
324
75044
                    .count();
325
71983
            }
326
        }
327

            
328
        // O(V + E): Compute the attractor set to the alpha-region.
329
49476
        while let Some(w) = self.todo.pop_front() {
330
            // Check all predecessors v of w.
331
63160
            for v in self.predecessors.predecessors(w) {
332
                // Skip predecessors that are already in the attractor set, also skip
333
                // vertices outside the subgame G <= prio. Or vertices that are computed.
334
63160
                if self.region_function[*v] == Some(prio)
335
31865
                    || self.region_function[*v].is_none()
336
29230
                    || (in_subgraph && self.region_function[*v].is_some_and(|region| region > prio))
337
                {
338
44414
                    continue;
339
18746
                }
340

            
341
18746
                if self.game.owner(v) == alpha {
342
8985
                    // sigma(v) = w, a valid strategy for alpha is to pick a successor in A.
343
8985
                    strategy.set(v, w);
344
8985
                } else {
345
                    // One more successor of v (namely w) entered the region. The
346
                    // opponent vertex v is only attracted once every successor
347
                    // that could enter the region has actually done so.
348
9761
                    self.attractor_counters[*v] -= 1;
349
9761
                    if self.attractor_counters[*v] != 0 {
350
4452
                        continue; // not in the attractor set yet!
351
5309
                    }
352

            
353
                    // For opponent controlled vertices no strategy exists, so
354
                    // every possible outgoing edge is losing.
355
                }
356

            
357
                // Add a vertex to their new region and remove from the old one.
358
14294
                let old_region = self.region_function[*v].expect("Unsolved vertices must have a region");
359
14294
                self.regions[old_region.value()] -= 1;
360
14294
                self.regions[prio.value()] += 1;
361

            
362
                // When this part is reached, all liberties of v are gone or v belongs
363
                // to alpha, so add vertex v to the attractor set.
364
14294
                self.region_function[*v] = Some(prio);
365
14294
                self.todo.push_back(v);
366
            }
367
        }
368

            
369
        // R \ domain(tau restricted to R*), essentially vertices in R belonging to
370
        // alpha where no strategy is defined yet. These can pick an arbitrary
371
        // successor that can reach R \ R*, these already have an attraction
372
        // strategy so that is always fine.
373
147027
        for &v in &self.unsolved {
374
147027
            if self.region_function[*v] == Some(prio) && self.game.owner(v) == alpha && strategy.get(v).is_none() {
375
12460
                for edge in self.game.outgoing_edges(v) {
376
12460
                    let w = edge.to();
377

            
378
12460
                    if self.region_function[*w] == Some(prio) {
379
                        // There exists some (v, w) in E such that w belongs to R (has r[w] == prio).
380
6924
                        strategy.set(v, w);
381
6924
                        break;
382
5536
                    }
383
                }
384
137352
            }
385
        }
386
2503
    }
387

            
388
    /// Determine whether the alpha-region with priority `prio` is open in G, or
389
    /// in G <= prio (indicated by `in_subgraph`). This means that for all vertices
390
    /// v with region_function\[v\] equal to prio, this is set R. When v belongs
391
    /// to alpha, determined by prio mod 2, there is some witness successor in R.
392
    /// For opponent vertices all successors lead to R, no witness to escape basically.
393
2660
    fn is_open(&self, prio: Priority, in_subgraph: bool) -> bool {
394
2660
        let alpha = Player::from_priority(prio);
395

            
396
        // O(V): Loop over unsolved vertices and find vertices belonging to region with prio.
397
82781
        for &v in &self.unsolved {
398
82781
            if self.region_function[*v] == Some(prio) {
399
26528
                if self.game.owner(v) != alpha {
400
                    // For all (v, u) in E, u should belong to R.
401
21277
                    for edge in self.game.outgoing_edges(v) {
402
21277
                        let u = edge.to();
403

            
404
                        // There is an edge from opponent to a vertex in the subgraph or in the whole graph.
405
21277
                        if self.region_function[*u].is_some()
406
21002
                            && ((in_subgraph && self.region_function[*u].is_some_and(|region| region < prio))
407
20282
                                || (!in_subgraph && self.region_function[*u] != Some(prio)))
408
                        {
409
877
                            return true;
410
20400
                        }
411
                    }
412
                } else {
413
                    // If there exists a (v, u) to R its closed.
414
7136
                    let is_open = self
415
7136
                        .game
416
7136
                        .outgoing_edges(v)
417
8768
                        .all(|edge| self.region_function[*edge.to()] != Some(prio));
418

            
419
7136
                    if is_open {
420
628
                        return true;
421
6508
                    }
422
                }
423
56253
            }
424
        }
425

            
426
1155
        false
427
2660
    }
428

            
429
    /// Promote a sub dominion D to the minimum region greater than prio that the
430
    /// opponent can reach. This updates region_function\[D -> prio\*\] and resets
431
    /// all priorities greater than prio\* to the original priority function. The
432
    /// strategy is updated by means of [`Self::compute_attractor`]. And lower strategies
433
    /// are set to `None` (no strategy known).
434
    ///
435
    /// # Details
436
    ///
437
    /// This is referred to as r\* = bep(R, r) in the paper (best escape priority).
438
    /// For every opponent vertex this is the highest priority that it can reach.
439
    /// The region it can reach belongs to alpha,
440
    /// otherwise it would be attracted in some earlier state.
441
157
    fn promote_sub_dominion<S: Strat>(&mut self, strategy: &mut S, prio: Priority) -> Priority {
442
157
        let alpha = Player::from_priority(prio);
443

            
444
        // O(V): It is only a dominion in the subgraph, determine lowest p > prio
445
        // that opponent can escape to.
446
157
        let mut promotion = Priority::new(self.regions.len() - 1);
447

            
448
10785
        for &v in &self.unsolved {
449
10785
            if self.region_function[*v] == Some(prio) && self.game.owner(v) != alpha {
450
                // For all (v, u) in E collect the lowest priority larger than prio that opponent can flee to.
451
3019
                for edge in self.game.outgoing_edges(v) {
452
3019
                    let u = edge.to();
453

            
454
3019
                    if let Some(region) = self.region_function[*u]
455
3019
                        && region > prio
456
202
                    {
457
202
                        promotion = promotion.min(region);
458
2817
                    }
459
                }
460
8037
            }
461
        }
462

            
463
157
        self.promotions += 1;
464

            
465
        // Here the prio region is promoted to the new priority and all lower positions
466
        // are reset.
467
10785
        for &v in &self.unsolved {
468
10785
            if self.region_function[*v] == Some(prio) {
469
3200
                // Promote the current region to the promotion priority.
470
3200
                self.regions[prio.value()] -= 1;
471
3200
                self.region_function[*v] = Some(promotion);
472
3200
                self.regions[promotion.value()] += 1;
473
7585
            } else if self.region_function[*v].is_some_and(|region| region < promotion) {
474
2829
                // Reset all vertices higher to the original priorities, remove the strategy.
475
2829
                let old_region = self.region_function[*v].expect("Unsolved vertices must have a region");
476
2829
                self.regions[old_region.value()] -= 1;
477
2829
                let original_prio = self.game.priority(v);
478
2829
                self.region_function[*v] = Some(original_prio);
479
2829
                strategy.remove(v);
480
2829
                self.regions[original_prio.value()] += 1;
481
4756
            }
482
        }
483

            
484
157
        promotion
485
157
    }
486

            
487
    /// Print the vertices with region_function\[v\] equal to prio, representing the region.
488
    ///
489
    /// This costs O(V) so only enable this in debug.
490
2004
    fn print_region(&self, prio: Priority) {
491
2004
        if log::log_enabled!(log::Level::Trace) {
492
            let vertices: Vec<_> = self
493
                .unsolved
494
                .iter()
495
                .filter(|&&v| self.region_function[*v] == Some(prio))
496
                .map(|v| v.value())
497
                .collect();
498
            trace!(
499
                "alpha-region[{}] = {{ {} }}",
500
                prio,
501
                vertices.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(",")
502
            );
503
2004
        }
504
2004
    }
505

            
506
    /// Computes max(rng(region_function <= prio)), so the next lower priority,
507
    /// smaller or equal to prio, that some vertex has.
508
    ///
509
    /// Starting from the current priority, find the next region that exists.
510
    /// This should never go out of bounds as the lowest region will always be a dominion.
511
1847
    fn next_priority(&self, prio: Priority) -> Priority {
512
1847
        let mut p = prio.value();
513
1972
        while self.regions[p] == 0 {
514
125
            assert!(p > 0, "next_priority went out of bounds");
515
125
            p -= 1;
516
        }
517
1847
        Priority::new(p)
518
1847
    }
519
}
520

            
521
#[cfg(test)]
522
mod tests {
523
    use merc_io::DumpFiles;
524
    use merc_utilities::random_test;
525

            
526
    use crate::random_parity_game;
527
    use crate::solve_zielonka;
528
    use crate::verify_solution;
529
    use crate::write_pg;
530

            
531
    use super::*;
532

            
533
    #[test]
534
    #[cfg_attr(miri, ignore)] // Miri is too slow for this test.
535
1
    fn test_random_priority_promotion_solver() {
536
100
        random_test(100, |rng| {
537
100
            let files = DumpFiles::new("test_random_priority_promotion_solver");
538
100
            let game = random_parity_game(rng, true, 100, 5, 3);
539

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

            
542
100
            let (solution, strategy) = solve_priority_promotion(&game, true);
543
100
            verify_solution(&game, &solution, &strategy.unwrap());
544
100
        });
545
1
    }
546

            
547
    #[test]
548
    #[cfg_attr(miri, ignore)]
549
1
    fn test_priority_promotion_matches_zielonka() {
550
100
        random_test(100, |rng| {
551
100
            let game = random_parity_game(rng, true, 50, 4, 3);
552

            
553
100
            let (pp_solution, _) = solve_priority_promotion(&game, false);
554
100
            let (zielonka_solution, _) = solve_zielonka(&game, false);
555

            
556
100
            assert_eq!(
557
100
                pp_solution[0], zielonka_solution[0],
558
                "Even winning sets differ between priority promotion and Zielonka"
559
            );
560
100
            assert_eq!(
561
100
                pp_solution[1], zielonka_solution[1],
562
                "Odd winning sets differ between priority promotion and Zielonka"
563
            );
564
100
        });
565
1
    }
566
}