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: i32,
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: &[usize], task: usize, d_max: usize) -> Option<usize> {
197    let start = task.checked_mul(d_max)?;
198    let end = start.checked_add(d_max)?;
199    let task_slice = config.get(start..end)?;
200    task_slice.iter().position(|&value| value == 1)
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: &[usize],
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(0) == 1;
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: &[usize],
229    source: &KSatisfiability<K3>,
230) -> Option<Vec<usize>> {
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] == 1;
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![0usize; construction.num_jobs * d_max];
309    for (job, slot) in task_slots.into_iter().enumerate() {
310        let slot = slot?;
311        config[job * d_max + slot] = 1;
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(&self, target_solution: &[usize]) -> Vec<usize> {
339        let d_max = self.target.d_max();
340        self.positive_start_jobs
341            .iter()
342            .map(|&job| usize::from(task_slot(target_solution, job, d_max) == Some(0)))
343            .collect()
344    }
345}
346
347#[reduction(
348    overhead = {
349        num_tasks = "(((2 * num_vars + 2) + 6 * num_clauses + sqrt(((2 * num_vars + 2) - 6 * num_clauses)^2)) / 2) * (num_vars + 3)",
350        num_processors = "((2 * num_vars + 2) + 6 * num_clauses + sqrt(((2 * num_vars + 2) - 6 * num_clauses)^2)) / 2",
351        d_max = "(((2 * num_vars + 2) + 6 * num_clauses + sqrt(((2 * num_vars + 2) - 6 * num_clauses)^2)) / 2) * (num_vars + 3)",
352    }
353)]
354impl ReduceTo<PreemptiveScheduling> for KSatisfiability<K3> {
355    type Result = Reduction3SATToPreemptiveScheduling;
356
357    fn reduce_to(&self) -> Self::Result {
358        let construction = build_ullman_construction(self);
359        let target = PreemptiveScheduling::new(
360            vec![1; construction.num_jobs],
361            construction.num_processors,
362            construction.precedences.clone(),
363        );
364
365        Reduction3SATToPreemptiveScheduling {
366            target,
367            positive_start_jobs: construction
368                .positive_chains
369                .iter()
370                .map(|chain| chain[0])
371                .collect(),
372            threshold: construction.time_limit,
373        }
374    }
375}
376
377#[cfg(feature = "example-db")]
378pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
379    use crate::export::SolutionPair;
380    use crate::models::formula::CNFClause;
381
382    vec![crate::example_db::specs::RuleExampleSpec {
383        id: "ksatisfiability_to_preemptivescheduling",
384        build: || {
385            let source = KSatisfiability::<K3>::new(3, vec![CNFClause::new(vec![1, 2, 3])]);
386            let reduction = ReduceTo::<PreemptiveScheduling>::reduce_to(&source);
387            let source_config = vec![0, 0, 1];
388            let target_config = construct_schedule_from_assignment(
389                reduction.target_problem(),
390                &source_config,
391                &source,
392            )
393            .expect("canonical example assignment should yield a schedule");
394            crate::example_db::specs::rule_example_with_witness::<_, PreemptiveScheduling>(
395                source,
396                SolutionPair {
397                    source_config,
398                    target_config,
399                },
400            )
401        },
402    }]
403}
404
405#[cfg(test)]
406#[path = "../unit_tests/rules/ksatisfiability_preemptivescheduling.rs"]
407mod tests;