Reduction
Explore the concept of reduction as a key technique in algorithm design. Understand how to solve one problem by leveraging solutions to another via black box subroutines, ensuring correctness while abstracting implementation details. This lesson highlights the importance of abstract data types and demonstrates how reduction facilitates building efficient and modular algorithms in C++.
We'll cover the following...
What is reduction?
Reduction is the single most common technique used in designing algorithms. Reducing one problem to another problem means to write an algorithm for that uses an algorithm for as a black box or subroutine. Crucially, the correctness of the resulting algorithm for can’t depend in any way on how the algorithm for works. The only thing we can assume is that the black box solves correctly. The inner workings of the black box are simply none of our business; they’re somebody else’s problem. It’s often best to literally think of the black box as functioning purely by magic.
Importance of abstract data types
For example, the peasant multiplication algorithm described in the previous chapter reduces the problem of multiplying two arbitrary positive integers to three simpler problems: addition, mediation (halving), and parity-checking. The algorithm relies on an abstract “positive integer” data type that supports those three operations, but the correctness of the multiplication algorithm doesn’t depend on the precise data representation (tally marks, clay tokens, Babylonian hexagesimal, quipu, counting rods, Roman numerals, finger positions, augrym stones, gobar numerals, binary, negabinary, Gray code, balanced ternary, phinary, quater-imaginary, . . . ) or on the precise implementations of those operations. Of course, the running time of the multiplication algorithm depends on the running time of the addition, mediation, and parity operations, but that’s a separate issue from correctness. Most importantly, we can create a more efficient multiplication algorithm just by switching to a more efficient number representation (from tally marks to place-value notation, for example).
Similarly, the Huntington-Hill algorithm reduces the problem of apportioning Congress to the problem of maintaining a priority queue that supports the operations and . The abstract data type priority queue is a black box; the correctness of the apportionment algorithm doesn’t depend on any specific priority queue data structure. Of course, the running time of the apportionment algorithm depends on the running time of the and algorithms, but that’s a separate issue from the correctness of the algorithm. The beauty of the reduction is that we can create a more efficient apportionment algorithm by simply swapping in a new priority queue data structure. Moreover, the designer of that data structure doesn’t need to know or care that it will be used to apportion Congress.
When we design algorithms, we may not know exactly how the basic building blocks we use are implemented or how our algorithms might be used as building blocks to solve even bigger problems. That ignorance is uncomfortable for many beginners, but it is both unavoidable and extremely useful. Even when you do know precisely how your components work, it’s often very helpful to pretend that you don’t.