NP-Completeness and Approximation Algorithms
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Why This Matters
The interview reality check
Now that we've covered Dynamic Programming, Greedy Algorithms, Graph Algorithms, Advanced Data Structures, and Bit Manipulation, you have a strong toolkit for solving many MAANG-style problems efficiently.
But some problems are different.
Sometimes an interviewer gives you a problem and the honest answer is not “use a clever data structure.” It is:
- The exact solution may be too slow for large inputs.
- The problem belongs to a class where no known polynomial-time algorithm exists.
- We should recognize the hardness, then choose a practical strategy.
That is where NP-completeness and approximation algorithms come in.
By the end, you should know how to say: “This looks computationally hard, so I’ll solve a restricted version, use exponential search for small n, or use an approximation.”

P, NP, Hard
Three buckets to recognize
Think of algorithmic problems by how hard they are to solve and verify.
P
Problems we can solve in polynomial time, like:
- Binary search:
O(log n) - BFS/DFS:
O(V + E) - Dijkstra with a heap: roughly
O(E log V)
These are the kinds of efficient solutions we practiced earlier.
NP
Problems where, if someone gives you a candidate answer, you can verify it in polynomial time.
Example: Subset Sum decision version
Given numbers and a target, is there some subset that sums to the target?
If someone hands you the subset, you can quickly add it up and check.
NP-complete
Problems that are both:
- In NP
- At least as hard as every problem in NP
If anyone finds a polynomial-time algorithm for one NP-complete problem, it would imply efficient solutions for all NP problems. That is the famous unsolved P vs NP question.

Decision Versions
Optimization vs decision
NP-completeness is usually discussed using decision problems, where the answer is yes/no.
Many interview-style problems are optimization problems:
Find the minimum number of servers, edges, coins, routes, or groups.
To discuss hardness, we often convert them into decision form:
Is there a solution using at most
kservers, edges, coins, routes, or groups?
This matters because decision versions are easier to compare through reductions.
Example
Optimization:
What is the minimum subset whose sum is exactly target?
Decision:
Does any subset sum to target?
Below is a small brute-force Java program for the decision version. It is correct, but it tries include/exclude choices, so it grows exponentially.
```java
public class Main {
static boolean subsetSum(int[] nums, int i, int remaining) {
if (remaining == 0) return true;
if (i == nums.length || remaining < 0) return false;
// Choice 1: include nums[i]
boolean take = subsetSum(nums, i + 1, remaining - nums[i]);
// Choice 2: skip nums[i]
boolean skip = subsetSum(nums, i + 1, remaining);
return take || skip;
}
public static void main(String[] args) {
int[] nums = {3, 34, 4, 12, 5, 2};
int target = 9;
System.out.println("Subset sum exists? " + subsetSum(nums, 0, target));
// Example subset: 4 + 5 = 9
}
}
```
Quick Check
A problem is in NP if:
Reductions Matter
How problems prove hardness
A reduction converts one problem into another.
If problem A can be transformed into problem B, then a fast solver for B would also solve A.
Input for A -> transform -> Input for B
solve B
Answer for A <- convert back <- Answer for B
This is the core idea behind NP-completeness proofs.
Interview-level intuition
You usually do not need to prove NP-completeness from scratch in coding interviews.
But you should recognize signals like:
- “Try all subsets”
- “Try all orderings”
- “Minimum set of choices covering constraints”
- “Can we satisfy all constraints?”
-
nis small, liken <= 20, suggesting exponential search may be acceptable
This connects directly to Recursion and Backtracking and Dynamic Programming: those tools still help, but they may not magically make every problem polynomial.

NP Complete Means
True or false?
If a problem is NP-complete, then no algorithm can ever solve it correctly.
Approximation Mindset
When exact is too expensive
For some NP-hard optimization problems, we use approximation algorithms.
An approximation algorithm gives a solution that is:
- Fast enough to run on large inputs
- Not always optimal
- Mathematically bounded compared to optimal, when possible
Example guarantee
A 2-approximation means:
The algorithm's answer is at most 2 times worse than the optimal answer.
For minimization:
algorithmCost <= 2 * optimalCost
For maximization, the comparison is usually reversed:
algorithmValue >= optimalValue / 2
This is different from the greedy lesson. A greedy algorithm is a strategy. An approximation algorithm is a strategy plus a quality guarantee.

Vertex Cover Approx
A classic approximation
A vertex cover in a graph is a set of vertices that touches every edge.
Problem:
Find the smallest vertex cover.
The exact minimum vertex cover problem is NP-hard for general graphs.
But there is a simple 2-approximation:
- Pick any uncovered edge
(u, v). - Add both
uandvto the cover. - Remove all edges touching
uorv. - Repeat.
Why the factor 2 intuition works:
- Every picked edge must be covered by any valid solution.
- The picked edges do not share endpoints after removal.
- Optimal needs at least one endpoint per picked edge.
- We take two endpoints per picked edge.
So our cover is at most twice the optimal size.
```java
import java.util.*;
public class Main {
static class Edge {
int u, v;
Edge(int u, int v) { this.u = u; this.v = v; }
public String toString() { return "(" + u + "," + v + ")"; }
}
static Set<Integer> approximateVertexCover(List<Edge> edges) {
Set<Integer> cover = new LinkedHashSet<>();
boolean[] removed = new boolean[edges.size()];
for (int i = 0; i < edges.size(); i++) {
if (removed[i]) continue;
Edge chosen = edges.get(i);
cover.add(chosen.u);
cover.add(chosen.v);
for (int j = 0; j < edges.size(); j++) {
Edge e = edges.get(j);
if (e.u == chosen.u || e.v == chosen.u ||
e.u == chosen.v || e.v == chosen.v) {
removed[j] = true;
}
}
}
return cover;
}
public static void main(String[] args) {
List<Edge> edges = Arrays.asList(
new Edge(0, 1), new Edge(1, 2), new Edge(2, 3),
new Edge(3, 4), new Edge(4, 5), new Edge(1, 5)
);
Set<Integer> cover = approximateVertexCover(edges);
System.out.println("Approx vertex cover: " + cover);
System.out.println("Cover size: " + cover.size());
}
}
```
Approx Ratio
Fill in the blank:
If an approximation algorithm for a minimization problem guarantees algorithmCost <= 2 * optimalCost, we call it a ___-approximation.
Fill in the blank: _____________
Set Cover Greedy
Another practical pattern
Set Cover is another famous NP-hard problem.
You have:
- A universe of items to cover
- Several sets, each covering some items
- Goal: choose as few sets as possible to cover everything
This appears in real systems as test selection, feature coverage, monitoring, ad placement, and resource planning.
A common approximation-style heuristic is:
Repeatedly pick the set that covers the largest number of currently uncovered items.
This greedy choice is not always optimal, but it is useful and has known approximation guarantees in theory.
Here is a small Java version.
```java
import java.util.*;
public class Main {
static List<String> greedySetCover(Set<String> universe, Map<String, Set<String>> sets) {
Set<String> uncovered = new HashSet<>(universe);
List<String> chosen = new ArrayList<>();
while (!uncovered.isEmpty()) {
String bestName = null;
int bestGain = 0;
for (Map.Entry<String, Set<String>> entry : sets.entrySet()) {
int gain = 0;
for (String item : entry.getValue()) {
if (uncovered.contains(item)) gain++;
}
if (gain > bestGain) {
bestGain = gain;
bestName = entry.getKey();
}
}
if (bestName == null) {
throw new IllegalStateException("Some items cannot be covered");
}
chosen.add(bestName);
uncovered.removeAll(sets.get(bestName));
}
return chosen;
}
public static void main(String[] args) {
Set<String> universe = new HashSet<>(Arrays.asList(
"arrays", "graphs", "dp", "greedy", "strings", "bits"
));
Map<String, Set<String>> sets = new LinkedHashMap<>();
sets.put("Plan A", new HashSet<>(Arrays.asList("arrays", "graphs", "dp")));
sets.put("Plan B", new HashSet<>(Arrays.asList("greedy", "strings")));
sets.put("Plan C", new HashSet<>(Arrays.asList("graphs", "bits")));
sets.put("Plan D", new HashSet<>(Arrays.asList("bits", "strings", "greedy")));
System.out.println("Chosen sets: " + greedySetCover(universe, sets));
}
}
```
Interview Playbook
What to say in interviews
When a problem starts looking NP-hard, do not panic. Use a structured response.
1. Clarify constraints
Ask about input size.
-
n <= 20: backtracking or bitmask DP may be fine. -
n <= 10^5: exact exponential search is probably impossible.
2. Identify the shape
Look for:
- Subsets
- Permutations
- Constraint satisfaction
- Minimum cover
- Scheduling with many interacting constraints
3. Offer levels of solution
A strong answer might sound like:
“The exact version appears exponential. For small inputs I can use backtracking with pruning. For larger inputs, I would use a greedy approximation and explain its tradeoff.”
4. Connect to your toolkit
Use what you already learned:
- Backtracking for exhaustive search
- DP for overlapping subproblems or bitmask states
- Greedy when you can prove optimality or provide an approximation guarantee
- Graphs and Hashing to model and speed up checks
Next, we will briefly zoom out from algorithms into how larger systems are designed.
