Skip to main content

problemreductions/rules/
ksatisfiability_preemptivescheduling.rs

1//! Reduction from KSatisfiability (3-SAT) to PreemptiveScheduling.
2//!
3//! This follows Ullman's 1975 construction via a unit-task precedence
4//! scheduling instance. Since every task has length 1, preemption is inert:
5//! the constructed instance is a valid preemptive scheduling problem whose
6//! optimal makespan hits the threshold `T = num_vars + 3` iff a nontrivial
7//! 3-SAT instance is satisfiable. Empty formulas and empty clauses use a
8//! one-task instance with bound 1 and 0 respectively.
9//!
10//! Reference: Jeffrey D. Ullman, "NP-complete scheduling problems", JCSS 10,
11//! 1975; Garey & Johnson, Appendix A5.2.
12
13use crate::models::formula::KSatisfiability;
14use crate::models::misc::PreemptiveScheduling;
15use crate::reduction;
16use crate::rules::traits::{ReduceTo, ReductionResult};
17use crate::variant::K3;
18
19#[derive(Debug, Clone)]
20struct UllmanConstruction {
21    time_limit: usize,
22    num_processors: usize,
23    positive_chains: Vec<Vec<usize>>,
24    #[cfg(any(test, feature = "example-db"))]
25    negative_chains: Vec<Vec<usize>>,
26    #[cfg(any(test, feature = "example-db"))]
27    positive_forcing: Vec<usize>,
28    #[cfg(any(test, feature = "example-db"))]
29    negative_forcing: Vec<usize>,
30    #[cfg(any(test, feature = "example-db"))]
31    clause_jobs: Vec<Vec<usize>>,
32    #[cfg(any(test, feature = "example-db"))]
33    filler_jobs_by_slot: Vec<Vec<usize>>,
34    precedences: Vec<(usize, usize)>,
35    num_jobs: usize,
36}
37
38fn time_limit(num_vars: usize) -> usize {
39    num_vars + 3
40}
41
42fn slot_capacities(num_vars: usize, num_clauses: usize) -> Vec<usize> {
43    let mut capacities = vec![0; time_limit(num_vars)];
44    capacities[0] = num_vars;
45    capacities[1] = 2 * num_vars + 1;
46    for capacity in capacities.iter_mut().take(num_vars + 1).skip(2) {
47        *capacity = 2 * num_vars + 2;
48    }
49    capacities[num_vars + 1] = num_clauses + num_vars + 1;
50    capacities[num_vars + 2] = 6 * num_clauses;
51    capacities
52}
53
54fn literal_endpoint(
55    literal: i64,
56    pick_literal: bool,
57    positive_chains: &[Vec<usize>],
58    negative_chains: &[Vec<usize>],
59    endpoint_step: usize,
60) -> usize {
61    let variable = literal.unsigned_abs() as usize - 1;
62    match (literal > 0, pick_literal) {
63        (true, true) | (false, false) => positive_chains[variable][endpoint_step],
64        (true, false) | (false, true) => negative_chains[variable][endpoint_step],
65    }
66}
67
68fn build_ullman_construction(source: &KSatisfiability<K3>) -> UllmanConstruction {
69    let num_vars = source.num_vars();
70    let num_clauses = source.num_clauses();
71    let time_limit = time_limit(num_vars);
72    let capacities = slot_capacities(num_vars, num_clauses);
73    // A nonempty filler layer in every slot forces the clock to span all T slots.
74    let num_processors = capacities.iter().max().unwrap() + 1;
75
76    let mut next_job = 0usize;
77
78    let mut positive_chains = vec![vec![0; num_vars + 1]; num_vars];
79    let mut negative_chains = vec![vec![0; num_vars + 1]; num_vars];
80    for variable in 0..num_vars {
81        for step in 0..=num_vars {
82            positive_chains[variable][step] = next_job;
83            next_job += 1;
84            negative_chains[variable][step] = next_job;
85            next_job += 1;
86        }
87    }
88
89    let positive_forcing: Vec<usize> = (0..num_vars)
90        .map(|_| {
91            let job = next_job;
92            next_job += 1;
93            job
94        })
95        .collect();
96    let negative_forcing: Vec<usize> = (0..num_vars)
97        .map(|_| {
98            let job = next_job;
99            next_job += 1;
100            job
101        })
102        .collect();
103
104    let mut clause_jobs = vec![vec![0; 7]; num_clauses];
105    for jobs in &mut clause_jobs {
106        for job in jobs {
107            *job = next_job;
108            next_job += 1;
109        }
110    }
111
112    let mut filler_jobs_by_slot = vec![Vec::new(); time_limit];
113    for slot in 0..time_limit {
114        let filler_count = num_processors - capacities[slot];
115        filler_jobs_by_slot[slot] = (0..filler_count)
116            .map(|_| {
117                let job = next_job;
118                next_job += 1;
119                job
120            })
121            .collect();
122    }
123
124    let mut precedences = Vec::new();
125
126    for variable in 0..num_vars {
127        for step in 0..num_vars {
128            precedences.push((
129                positive_chains[variable][step],
130                positive_chains[variable][step + 1],
131            ));
132            precedences.push((
133                negative_chains[variable][step],
134                negative_chains[variable][step + 1],
135            ));
136        }
137        precedences.push((
138            positive_chains[variable][variable],
139            positive_forcing[variable],
140        ));
141        precedences.push((
142            negative_chains[variable][variable],
143            negative_forcing[variable],
144        ));
145    }
146
147    for (clause_index, clause) in source.clauses().iter().enumerate() {
148        for (pattern_index, &clause_job) in clause_jobs[clause_index].iter().enumerate() {
149            let pattern = pattern_index + 1;
150            // Repeating literals preserves shorter nonempty disjunctions.
151            for (position, &literal) in clause.literals.iter().cycle().take(3).enumerate() {
152                let bit_is_one = ((pattern >> (2 - position)) & 1) == 1;
153                precedences.push((
154                    literal_endpoint(
155                        literal,
156                        bit_is_one,
157                        &positive_chains,
158                        &negative_chains,
159                        num_vars,
160                    ),
161                    clause_job,
162                ));
163            }
164        }
165    }
166
167    for slot in 0..time_limit.saturating_sub(1) {
168        for &pred in &filler_jobs_by_slot[slot] {
169            for &succ in &filler_jobs_by_slot[slot + 1] {
170                precedences.push((pred, succ));
171            }
172        }
173    }
174
175    UllmanConstruction {
176        time_limit,
177        num_processors,
178        positive_chains,
179        #[cfg(any(test, feature = "example-db"))]
180        negative_chains,
181        #[cfg(any(test, feature = "example-db"))]
182        positive_forcing,
183        #[cfg(any(test, feature = "example-db"))]
184        negative_forcing,
185        #[cfg(any(test, feature = "example-db"))]
186        clause_jobs,
187        #[cfg(any(test, feature = "example-db"))]
188        filler_jobs_by_slot,
189        precedences,
190        num_jobs: next_job,
191    }
192}
193
194#[cfg(any(test, feature = "example-db"))]
195fn set_task_slot(task_slots: &mut [Option<usize>], job: usize, slot: usize) {
196    task_slots[job] = Some(slot);
197}
198
199#[cfg(any(test, feature = "example-db"))]
200fn clause_pattern_for_assignment(
201    clause: &crate::models::formula::CNFClause,
202    assignment: &[bool],
203) -> usize {
204    let mut pattern = 0usize;
205    for (position, &literal) in clause.literals.iter().cycle().take(3).enumerate() {
206        let variable = literal.unsigned_abs() as usize - 1;
207        let value = assignment[variable];
208        let literal_true = if literal > 0 { value } else { !value };
209        if literal_true {
210            pattern |= 1 << (2 - position);
211        }
212    }
213    pattern
214}
215
216#[cfg(any(test, feature = "example-db"))]
217fn construct_schedule_from_assignment(
218    target: &PreemptiveScheduling,
219    assignment: &[bool],
220    source: &KSatisfiability<K3>,
221) -> Option<Vec<Vec<bool>>> {
222    if source.num_clauses() == 0 || source.clauses().iter().any(|c| c.literals.is_empty()) {
223        return crate::traits::Problem::evaluate(source, &assignment.to_vec())
224            .unwrap()
225            .0
226            .then(|| vec![vec![true]]);
227    }
228    let construction = build_ullman_construction(source);
229    if assignment.len() != source.num_vars() || target.num_tasks() != construction.num_jobs {
230        return None;
231    }
232
233    let mut task_slots = vec![None; construction.num_jobs];
234
235    for variable in 0..source.num_vars() {
236        let value_is_true = assignment[variable];
237        for step in 0..=source.num_vars() {
238            if value_is_true {
239                set_task_slot(
240                    &mut task_slots,
241                    construction.positive_chains[variable][step],
242                    step,
243                );
244                set_task_slot(
245                    &mut task_slots,
246                    construction.negative_chains[variable][step],
247                    step + 1,
248                );
249            } else {
250                set_task_slot(
251                    &mut task_slots,
252                    construction.negative_chains[variable][step],
253                    step,
254                );
255                set_task_slot(
256                    &mut task_slots,
257                    construction.positive_chains[variable][step],
258                    step + 1,
259                );
260            }
261        }
262
263        let positive_slot = task_slots[construction.positive_chains[variable][variable]]
264            .expect("positive chain slot is assigned");
265        let negative_slot = task_slots[construction.negative_chains[variable][variable]]
266            .expect("negative chain slot is assigned");
267        set_task_slot(
268            &mut task_slots,
269            construction.positive_forcing[variable],
270            positive_slot + 1,
271        );
272        set_task_slot(
273            &mut task_slots,
274            construction.negative_forcing[variable],
275            negative_slot + 1,
276        );
277    }
278
279    for (clause_index, clause) in source.clauses().iter().enumerate() {
280        let pattern = clause_pattern_for_assignment(clause, assignment);
281        if pattern == 0 {
282            return None;
283        }
284        for pattern_index in 0..7 {
285            let slot = if pattern_index + 1 == pattern {
286                source.num_vars() + 1
287            } else {
288                source.num_vars() + 2
289            };
290            set_task_slot(
291                &mut task_slots,
292                construction.clause_jobs[clause_index][pattern_index],
293                slot,
294            );
295        }
296    }
297
298    for (slot, fillers) in construction.filler_jobs_by_slot.iter().enumerate() {
299        for &job in fillers {
300            set_task_slot(&mut task_slots, job, slot);
301        }
302    }
303
304    let d_max = target.d_max();
305    let mut config = vec![vec![false; d_max]; construction.num_jobs];
306    for (job, slot) in task_slots.into_iter().enumerate() {
307        let slot = slot?;
308        config[job][slot] = true;
309    }
310    Some(config)
311}
312
313/// Result of reducing KSatisfiability<K3> to PreemptiveScheduling.
314#[derive(Debug, Clone)]
315pub struct Reduction3SATToPreemptiveScheduling {
316    target: PreemptiveScheduling,
317    positive_start_jobs: Vec<usize>,
318    threshold: usize,
319}
320
321impl Reduction3SATToPreemptiveScheduling {
322    pub fn threshold(&self) -> usize {
323        self.threshold
324    }
325}
326
327impl ReductionResult for Reduction3SATToPreemptiveScheduling {
328    type Source = KSatisfiability<K3>;
329    type Target = PreemptiveScheduling;
330
331    fn target_problem(&self) -> &Self::Target {
332        &self.target
333    }
334
335    fn extract_solution(
336        &self,
337        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
338    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
339        crate::rules::traits::validate_target_witness(
340            self.target_problem(),
341            target_solution,
342            |value| crate::rules::AggregateReductionResult::extract_value(self, value).0,
343            "target schedule does not meet the satisfiability threshold",
344        )?;
345        Ok(self
346            .positive_start_jobs
347            .iter()
348            .map(|&job| target_solution[job][0])
349            .collect())
350    }
351}
352
353#[crate::aggregate_reduction]
354impl crate::rules::AggregateReductionResult for Reduction3SATToPreemptiveScheduling {
355    type Source = KSatisfiability<K3>;
356    type Target = PreemptiveScheduling;
357
358    fn target_problem(&self) -> &Self::Target {
359        &self.target
360    }
361
362    fn extract_value(&self, value: crate::types::Min<i64>) -> crate::types::Or {
363        crate::types::Or(
364            value
365                .0
366                .is_some_and(|makespan| i128::from(makespan) <= self.threshold as i128),
367        )
368    }
369}
370
371// Variable chains/forcing contribute 2n(n+1) precedences; clauses contribute
372// 21c, retaining repeated links. Each of n+2 adjacent filler layers has at
373// most P^2 links, P<=2n+3+6c. Unit lengths give d_max<=P(n+3); both factors
374// are <2^(n+c+3) and <2^(n+2), respectively. Early outputs have one unit task.
375#[reduction(transform = upper_bound {
376    num_tasks = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)",
377    num_processors = "2 * num_vars + 3 + 6 * num_clauses",
378    d_max = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)",
379    max_schedule_magnitude_bits = "2 * num_vars + num_clauses + 5",
380    num_precedences = "2 * num_vars * (num_vars + 1) + 21 * num_clauses + (num_vars + 2) * (2 * num_vars + 3 + 6 * num_clauses)^2",
381})]
382impl ReduceTo<PreemptiveScheduling> for KSatisfiability<K3> {
383    type Result = Reduction3SATToPreemptiveScheduling;
384
385    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
386        let has_empty_clause = self.clauses().iter().any(|c| c.literals.is_empty());
387        if self.num_clauses() == 0 || has_empty_clause {
388            // A one-task schedule meets bound 1, but cannot meet bound 0.
389            return Ok(Reduction3SATToPreemptiveScheduling {
390                target: PreemptiveScheduling::new(vec![1], 1, vec![])
391                    .map_err(<Self as ReduceTo<PreemptiveScheduling>>::target_construction)?,
392                positive_start_jobs: vec![0; self.num_vars()],
393                threshold: usize::from(!has_empty_clause),
394            });
395        }
396        let construction = build_ullman_construction(self);
397        let target = PreemptiveScheduling::new(
398            vec![1_i64; construction.num_jobs],
399            construction.num_processors,
400            construction.precedences.clone(),
401        )
402        .map_err(
403            crate::rules::ReductionError::construction::<KSatisfiability<K3>, PreemptiveScheduling>,
404        )?;
405
406        Ok(Reduction3SATToPreemptiveScheduling {
407            target,
408            positive_start_jobs: construction
409                .positive_chains
410                .iter()
411                .map(|chain| chain[0])
412                .collect(),
413            threshold: construction.time_limit,
414        })
415    }
416}
417
418#[cfg(feature = "example-db")]
419pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
420    use crate::export::SolutionPair;
421    use crate::models::formula::CNFClause;
422
423    vec![crate::example_db::specs::RuleExampleSpec {
424        id: "ksatisfiability_to_preemptivescheduling",
425        build: || {
426            let source = KSatisfiability::<K3>::new(3, vec![CNFClause::new(vec![1, 2, 3])]);
427            let reduction = ReduceTo::<PreemptiveScheduling>::reduce_to(&source)
428                .expect("reduction should succeed");
429            let source_config = vec![false, false, true];
430            let target_config = construct_schedule_from_assignment(
431                reduction.target_problem(),
432                &source_config,
433                &source,
434            )
435            .expect("canonical example assignment should yield a schedule");
436            crate::example_db::specs::rule_example_with_witness::<_, PreemptiveScheduling>(
437                source,
438                SolutionPair {
439                    source_config: serde_json::to_value(source_config)
440                        .expect("solution serialization must succeed"),
441                    target_config: serde_json::to_value(target_config)
442                        .expect("solution serialization must succeed"),
443                },
444            )
445        },
446    }]
447}
448
449#[cfg(test)]
450#[path = "../unit_tests/rules/ksatisfiability_preemptivescheduling.rs"]
451mod tests;