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