3385. Minimum Time to Break Locks II π
Problem Description
Bob is trapped in a dungeon and must break n
locks to escape. Each lock requires a specific amount of energy to break, which is represented in an array named strength
. The array element strength[i]
indicates the energy needed to break the i^th
lock.
Bob uses a sword with these properties:
- The sword's initial energy is 0.
- The energy increase factor
X
starts at 1. - The sword's energy increases by
X
every minute. - To break lock
i
, the sword's energy must at least matchstrength[i]
. - The energy resets to 0 after breaking a lock, and the factor
X
increases by 1.
The challenge is to determine the minimum time, in minutes, required to break all n
locks.
Intuition
The problem can be visualized as a sequence where Bob needs to adjust the energy output of his sword to precisely meet or exceed the specific energy requirements of the locks. Each lock changes the dynamics of the sword's energy generation by increasing factor X
after being broken.
The solution can be framed as a flow optimization problem, where we aim to minimize the total time to break all locks while considering the changing factor for energy augmentation. Bob's task can be translated into a network flow issue with minimal cost, where the nodes represent locks and energy factors over time, and the edges represent the flow of energy consumption matched with lock requirements.
We construct a graph that models this scenario:
- Each lock and energy factor iteration forms nodes in the graph.
- Directed edges link these nodes with capacities and costs adjusted to represent the energy requirement divided by the current factor
X
. - A minimum cost flow algorithm navigates this graph from a starting node (source) to an ending node (sink), accounting for each lock's energy challenge while minimizing the total time.
This approach utilizes variables like the initial source and capacity constraints to map out feasible paths to quickly resolve the problem mathematically, choosing the most time-efficient sequence of actions for Bob to break all locks.
Learn more about Depth-First Search and Graph patterns.
Solution Approach
The solution uses a Minimum Cost Flow (MCF) graph algorithm to strategically break all locks in the minimum time. Key aspects of this implementation are as follows:
-
Graph Construction:
- We represent the scenario with a directed graph where nodes signify locks over several minute iterations.
- The graph has a structure with
2*n + 2
nodes, wheren
is the number of locks. Two additional nodes include a sources
and a sinkt
.
-
Edges and Capacities:
- An edge with a capacity of
1
and a cost of0
is added from the source nodes
to each lock node, representing that one lock can be broken at a time. - Each node representing a lock is connected to a corresponding node offset by
n
(as energy resets after a lock is broken), also with a capacity of1
. - Finally, these offset nodes are connected to the sink node
t
.
- An edge with a capacity of
-
Calculating Cost:
- The cost of each edge from lock nodes to their offsets is calculated by
(strength[i] - 1) // (j + 1) + 1
wherej
starts from0
. This formula calculates the minimum time required for the sword's energy to meet or exceed the lock's strength based on the increasing factorX
.
- The cost of each edge from lock nodes to their offsets is calculated by
-
Minimum Cost Flow Calculation:
- We employ the Min-Cost Flow algorithm to determine the minimum cumulative time required to break all locks when traversing from source
s
to sinkt
. - The function
flow(s, t, n)
computes this flow by accommodating the energy requirements against each lock's time constraints, ensuring that the flow (representing time) respects capacity constraints.
- We employ the Min-Cost Flow algorithm to determine the minimum cumulative time required to break all locks when traversing from source
This algorithm optimally utilizes an advanced graph-based dynamic programming technique, efficiently solving the minimum time problem using shortest path cost concepts embedded within a flow network.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example to illustrate the problem-solving approach for breaking the locks with the minimum time using Minimum Cost Flow.
Consider an example where Bob has to break 3
locks with a strength requirement array strength = [3, 2, 5]
.
-
Initial Setup:
- Locks need energy requirements of
3
,2
, and5
respectively.
- Locks need energy requirements of
-
Graph Construction:
- We construct a directed graph with
2 * 3 + 2 = 8
nodes. Nodes represent the initial and offset lock states as well as a source (s
) and a sink (t
). - The source node
s
connects to lock nodes with edges of capacity1
to signify the ability to approach each lock once.
- We construct a directed graph with
-
Edges and Costs:
- For each lock at node
i
, we connect it to its offset node ati + n
with capacity1
. The offset node model resets the energy level after breaking a lock. - The cost for each lock node
i
heading towards its offset is computed byceil(strength[i] / (X_j + 1))
, wherej
is the current round of that lock accounted for in X's increments.
- For each lock at node
-
Calculating Minimum Time:
- For lock
0
with strength3
, the cost to offset would be3
whenX = 1
. Lock1
with strength2
requires only2/1 = 2
time whenX = 1
. Lock2
requires5
time or more. - All offset nodes point towards the sink node
t
.
- For lock
-
Min-Cost Flow Execution:
- Implement a Min-Cost Flow algorithm to find the minimum time path from
s
tot
. - Here, Bob's initial energy increases by
X = 1
every minute across all locks. He moves to break locks in the order that minimizes cumulative time. - Breaking the second lock first requires
2
minutes (as the sword's energy has to match or exceed2
), resetting X. The X factor then becomes2
for subsequent locks incrementing energy output. Therefore, lock1
takes2
minutes; after increasingX = 2
, lock0
or lock2
decision will depend upon cumulative cost/time.
Using this structured flow, Bob would break all locks in minimized cumulative time, achieving the task optimally.
- Implement a Min-Cost Flow algorithm to find the minimum time path from
By this systematic implementation, we efficiently tackled the requirement minimizing Bob's time within the permissioned flow of energy through progressive graph traversals and cost evaluations.
Solution Implementation
1from typing import List, Tuple, Optional, NamedTuple, cast
2from heapq import heappop, heappush
3
4class MCFGraph:
5 class Edge(NamedTuple):
6 src: int # Source node
7 dst: int # Destination node
8 cap: int # Capacity of the edge
9 flow: int # Flow through the edge
10 cost: int # Cost of the flow
11
12 class _Edge:
13 def __init__(self, dst: int, cap: int, cost: int) -> None:
14 self.dst = dst # Destination node
15 self.cap = cap # Capacity
16 self.cost = cost # Cost
17 self.rev: Optional[MCFGraph._Edge] = None # Reverse edge
18
19 def __init__(self, n: int) -> None:
20 self._n = n # Number of nodes
21 self._g: List[List[MCFGraph._Edge]] = [[] for _ in range(n)] # Adjacency list to hold edges
22 self._edges: List[MCFGraph._Edge] = [] # List to hold all edges for quick access
23
24 def add_edge(self, src: int, dst: int, cap: int, cost: int) -> int:
25 # Add an edge from src to dst with capacity cap and cost
26 assert 0 <= src < self._n
27 assert 0 <= dst < self._n
28 assert 0 <= cap
29 m = len(self._edges)
30
31 # Create the edge and its reverse edge
32 e = MCFGraph._Edge(dst, cap, cost)
33 re = MCFGraph._Edge(src, 0, -cost)
34
35 # Link reverse edges
36 e.rev = re
37 re.rev = e
38
39 # Append the edges to their respective nodes
40 self._g[src].append(e)
41 self._g[dst].append(re)
42
43 # Maintain a list of edges for easy access
44 self._edges.append(e)
45
46 return m
47
48 def get_edge(self, i: int) -> Edge:
49 # Retrieve the edge details and construct the user-facing Edge named tuple
50 assert 0 <= i < len(self._edges)
51 e = self._edges[i]
52 re = cast(MCFGraph._Edge, e.rev)
53 return MCFGraph.Edge(re.dst, e.dst, e.cap + re.cap, re.cap, e.cost)
54
55 def edges(self) -> List[Edge]:
56 # Return all edges in the graph as a list of Edge named tuples
57 return [self.get_edge(i) for i in range(len(self._edges))]
58
59 def flow(self, s: int, t: int, flow_limit: Optional[int] = None) -> Tuple[int, int]:
60 # Calculate flow from source s to sink t, with optional flow_limit
61 return self.slope(s, t, flow_limit)[-1]
62
63 def slope(self, s: int, t: int, flow_limit: Optional[int] = None) -> List[Tuple[int, int]]:
64 # Calculate the cost flow slope. Outputs tuples of (flow, cost)
65 assert 0 <= s < self._n
66 assert 0 <= t < self._n
67 assert s != t
68
69 if flow_limit is None:
70 # If no flow limit, set as the sum of capacities exiting s
71 flow_limit = cast(int, sum(e.cap for e in self._g[s]))
72
73 dual = [0] * self._n # Dual costs initialization
74 prev: List[Optional[Tuple[int, MCFGraph._Edge]]] = [None] * self._n
75
76 # Helper function to refine dual costs using Dijkstra's algorithm
77 def refine_dual() -> bool:
78 pq = [(0, s)] # Priority queue for shortest path
79 visited = [False] * self._n
80 dist: List[Optional[int]] = [None] * self._n
81 dist[s] = 0
82
83 while pq:
84 dist_v, v = heappop(pq)
85
86 if visited[v]:
87 continue
88
89 visited[v] = True
90
91 if v == t:
92 break # Found shortest path to t
93
94 dual_v = dual[v]
95 for e in self._g[v]:
96 w = e.dst
97 if visited[w] or e.cap == 0:
98 continue
99 reduced_cost = e.cost - dual[w] + dual_v
100 new_dist = dist_v + reduced_cost
101 dist_w = dist[w]
102 if dist_w is None or new_dist < dist_w:
103 dist[w] = new_dist
104 prev[w] = v, e
105 heappush(pq, (new_dist, w))
106 else:
107 return False # No path to t
108
109 dist_t = dist[t]
110 for v in range(self._n):
111 if visited[v]:
112 dual[v] -= cast(int, dist_t) - cast(int, dist[v])
113 return True
114
115 flow = 0
116 cost = 0
117 prev_cost_per_flow: Optional[int] = None
118 result = [(flow, cost)]
119
120 while flow < flow_limit:
121 if not refine_dual():
122 break
123 f = flow_limit - flow
124 v = t
125 while prev[v] is not None:
126 u, e = cast(Tuple[int, MCFGraph._Edge], prev[v])
127 f = min(f, e.cap)
128 v = u
129 v = t
130 while prev[v] is not None:
131 u, e = cast(Tuple[int, MCFGraph._Edge], prev[v])
132 e.cap -= f
133 assert e.rev is not None
134 e.rev.cap += f
135 v = u
136 c = -dual[s]
137 flow += f
138 cost += f * c
139 if c == prev_cost_per_flow:
140 result.pop()
141 result.append((flow, cost))
142 prev_cost_per_flow = c
143 return result
144
145
146class Solution:
147 def findMinimumTime(self, a: List[int]) -> int:
148 n = len(a)
149 s = n * 2 # Source node index
150 t = s + 1 # Sink node index
151 g = MCFGraph(t + 1) # Min-cost flow graph
152
153 # Connecting source to tasks and tasks to jobs
154 for i in range(n):
155 g.add_edge(s, i, 1, 0)
156 g.add_edge(i + n, t, 1, 0)
157
158 for j in range(n):
159 # Compute cost based on task effort and job position
160 g.add_edge(i, j + n, 1, (a[i] - 1) // (j + 1) + 1)
161
162 # Find minimum time to complete all tasks by computing flow
163 return g.flow(s, t, n)[1]
164
1import java.util.*;
2
3class MCFGraph {
4 // Define an edge with source, destination, capacity, flow, and cost
5 static class Edge {
6 int src, dst, cap, flow, cost;
7
8 Edge(int src, int dst, int cap, int flow, int cost) {
9 this.src = src;
10 this.dst = dst;
11 this.cap = cap;
12 this.flow = flow;
13 this.cost = cost;
14 }
15 }
16
17 // Internal edge representation with a destination, capacity, cost, and reverse edge
18 static class _Edge {
19 int dst, cap, cost;
20 _Edge rev;
21
22 _Edge(int dst, int cap, int cost) {
23 this.dst = dst;
24 this.cap = cap;
25 this.cost = cost;
26 }
27 }
28
29 private int numVertices;
30 private List<List<_Edge>> graph;
31 private List<_Edge> edges;
32
33 public MCFGraph(int numVertices) {
34 this.numVertices = numVertices;
35 this.graph = new ArrayList<>();
36 this.edges = new ArrayList<>();
37 for (int i = 0; i < numVertices; i++) {
38 graph.add(new ArrayList<>());
39 }
40 }
41
42 // Add an edge from src to dst with the given capacity and cost
43 public int addEdge(int src, int dst, int cap, int cost) {
44 assert (0 <= src && src < numVertices);
45 assert (0 <= dst && dst < numVertices);
46 assert (cap >= 0);
47
48 int m = edges.size();
49 _Edge edge = new _Edge(dst, cap, cost);
50 _Edge revEdge = new _Edge(src, 0, -cost);
51 edge.rev = revEdge;
52 revEdge.rev = edge;
53
54 graph.get(src).add(edge);
55 graph.get(dst).add(revEdge);
56 edges.add(edge);
57 return m;
58 }
59
60 // Get edge details including source, destination, capacity, and cost
61 public Edge getEdge(int i) {
62 assert (0 <= i && i < edges.size());
63 _Edge e = edges.get(i);
64 _Edge rev = e.rev;
65 return new Edge(rev.dst, e.dst, e.cap + rev.cap, rev.cap, e.cost);
66 }
67
68 // Retrieve a list of all edges in the graph
69 public List<Edge> edges() {
70 List<Edge> result = new ArrayList<>();
71 for (int i = 0; i < edges.size(); i++) {
72 result.add(getEdge(i));
73 }
74 return result;
75 }
76
77 // Compute the flow from source to target with an optional flow limit
78 public int[] flow(int src, int dst, Integer flowLimit) {
79 List<int[]> result = slope(src, dst, flowLimit);
80 return result.get(result.size() - 1);
81 }
82
83 // Implementation of the slope algorithm for min-cost max flow
84 public List<int[]> slope(int src, int dst, Integer flowLimit) {
85 assert (0 <= src && src < numVertices);
86 assert (0 <= dst && dst < numVertices);
87 assert (src != dst);
88
89 // If flow limit is not provided, assume maximum possible flow
90 if (flowLimit == null) {
91 flowLimit = graph.get(src).stream().mapToInt(e -> e.cap).sum();
92 }
93
94 int[] duals = new int[numVertices];
95 Tuple[] prev = new Tuple[numVertices];
96
97 List<int[]> result = new ArrayList<>();
98 result.add(new int[]{0, 0});
99
100 while (true) {
101 if (!refineDual(src, dst, duals, prev)) {
102 break;
103 }
104
105 int f = flowLimit;
106 int v = dst;
107
108 // Calculate the path flow
109 while (prev[v] != null) {
110 Tuple tuple = prev[v];
111 int u = tuple.first;
112 _Edge edge = tuple.second;
113 f = Math.min(f, edge.cap);
114 v = u;
115 }
116
117 // Adjust the flow along the path
118 v = dst;
119 while (prev[v] != null) {
120 Tuple tuple = prev[v];
121 int u = tuple.first;
122 _Edge edge = tuple.second;
123 edge.cap -= f;
124 edge.rev.cap += f;
125 v = u;
126 }
127
128 int c = -duals[src];
129 result.add(new int[]{
130 result.get(result.size() - 1)[0] + f,
131 result.get(result.size() - 1)[1] + f * c
132 });
133
134 if (c == result.get(result.size() - 2)[1]) {
135 result.remove(result.size() - 2);
136 }
137 }
138
139 return result;
140 }
141
142 // Use Dijkstra-like algorithm to update dual potentials
143 private boolean refineDual(int src, int dst, int[] dual, Tuple[] prev) {
144 PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
145 pq.add(new int[]{0, src});
146 boolean[] visited = new boolean[numVertices];
147 Integer[] dist = new Integer[numVertices];
148 Arrays.fill(dist, null);
149 dist[src] = 0;
150
151 while (!pq.isEmpty()) {
152 int[] current = pq.poll();
153 int currentDist = current[0];
154 int v = current[1];
155
156 if (visited[v]) continue;
157 visited[v] = true;
158
159 if (v == dst) break;
160
161 int dualV = dual[v];
162
163 // Relax edges
164 for (_Edge edge : graph.get(v)) {
165 int w = edge.dst;
166
167 if (visited[w] || edge.cap == 0) continue;
168
169 int reducedCost = edge.cost - dual[w] + dualV;
170 int newDist = currentDist + reducedCost;
171 Integer distW = dist[w];
172
173 if (distW == null || newDist < distW) {
174 dist[w] = newDist;
175 prev[w] = new Tuple(v, edge);
176 pq.add(new int[]{newDist, w});
177 }
178 }
179 }
180
181 if (!visited[dst]) return false;
182
183 // Update duals after settling at target
184 int distT = dist[dst];
185 for (int v = 0; v < numVertices; v++) {
186 if (visited[v]) {
187 dual[v] -= distT - dist[v];
188 }
189 }
190
191 return true;
192 }
193
194 // Helper class to store pairs of node and edge
195 static class Tuple {
196 int first;
197 _Edge second;
198
199 Tuple(int first, _Edge second) {
200 this.first = first;
201 this.second = second;
202 }
203 }
204}
205
206class Solution {
207 public int findMinimumTime(int[] strength) {
208 int n = strength.length;
209 int source = n * 2;
210 int target = source + 1;
211 MCFGraph graph = new MCFGraph(target + 1);
212
213 // Add edges from the source to each worker and from each job to the target
214 for (int i = 0; i < n; i++) {
215 graph.addEdge(source, i, 1, 0);
216 graph.addEdge(i + n, target, 1, 0);
217
218 // Add edges from each worker to each job with cost determined by the time taken
219 for (int j = 0; j < n; j++) {
220 graph.addEdge(i, j + n, 1, (strength[i] - 1) / (j + 1) + 1);
221 }
222 }
223
224 // Compute min-cost max flow and return the total minimum cost
225 return graph.flow(source, target, n)[1];
226 }
227}
228
1#include <bits/stdc++.h>
2
3using namespace std;
4
5// Define an edge with source, destination, capacity, flow, and cost
6struct Edge {
7 int src, dst, cap, flow, cost;
8
9 Edge(int src, int dst, int cap, int flow, int cost)
10 : src(src), dst(dst), cap(cap), flow(flow), cost(cost) {}
11};
12
13// Internal edge representation with a destination, capacity, cost, and reverse edge
14struct _Edge {
15 int dst, cap, cost;
16 _Edge* rev;
17
18 _Edge(int dst, int cap, int cost)
19 : dst(dst), cap(cap), cost(cost), rev(nullptr) {}
20};
21
22class MCFGraph {
23private:
24 int numVertices;
25 vector<vector<_Edge*>> graph;
26 vector<_Edge> edges;
27
28public:
29 MCFGraph(int numVertices)
30 : numVertices(numVertices), graph(numVertices) {}
31
32 // Add an edge from src to dst with the given capacity and cost
33 int addEdge(int src, int dst, int cap, int cost) {
34 assert(0 <= src && src < numVertices);
35 assert(0 <= dst && dst < numVertices);
36 assert(cap >= 0);
37
38 _Edge forward(dst, cap, cost);
39 _Edge backward(src, 0, -cost);
40
41 forward.rev = &backward;
42 backward.rev = &forward;
43
44 graph[src].push_back(&edges.emplace_back(dst, cap, cost));
45 graph[dst].push_back(&edges.emplace_back(src, 0, -cost));
46
47 return static_cast<int>(edges.size()) - 2;
48 }
49
50 // Get edge details including source, destination, capacity, and cost
51 Edge getEdge(int i) const {
52 assert(0 <= i && i < edges.size());
53 const _Edge* e = &edges[i];
54 const _Edge* rev = e->rev;
55 return Edge(rev->dst, e->dst, e->cap + rev->cap, rev->cap, e->cost);
56 }
57
58 // Retrieve a list of all edges in the graph
59 vector<Edge> edgesList() const {
60 vector<Edge> result;
61 for (int i = 0; i < edges.size(); i++) {
62 result.push_back(getEdge(i));
63 }
64 return result;
65 }
66
67 // Compute the flow from source to target with an optional flow limit
68 vector<int> flow(int src, int dst, int flowLimit) {
69 vector<vector<int>> result = slope(src, dst, flowLimit);
70 return result.back();
71 }
72
73 // Implementation of the slope algorithm for min-cost max flow
74 vector<vector<int>> slope(int src, int dst, int flowLimit) {
75 assert(0 <= src && src < numVertices);
76 assert(0 <= dst && dst < numVertices);
77 assert(src != dst);
78
79 // If flow limit is not provided, assume maximum possible flow
80 if (flowLimit == -1) {
81 flowLimit = accumulate(graph[src].begin(), graph[src].end(), 0, [](int cum, _Edge* e) {
82 return cum + e->cap;
83 });
84 }
85
86 vector<int> duals(numVertices);
87 vector<pair<int, _Edge*>> prev(numVertices);
88
89 vector<vector<int>> result;
90 result.push_back({0, 0});
91
92 while (true) {
93 if (!refineDual(src, dst, duals, prev)) {
94 break;
95 }
96
97 int f = flowLimit;
98 int v = dst;
99
100 // Calculate the path flow
101 while (prev[v].second != nullptr) {
102 int u = prev[v].first;
103 _Edge* edge = prev[v].second;
104 f = min(f, edge->cap);
105 v = u;
106 }
107
108 // Adjust the flow along the path
109 v = dst;
110 while (prev[v].second != nullptr) {
111 int u = prev[v].first;
112 _Edge* edge = prev[v].second;
113 edge->cap -= f;
114 edge->rev->cap += f;
115 v = u;
116 }
117
118 int c = -duals[src];
119 result.push_back({result.back()[0] + f, result.back()[1] + f * c});
120
121 if (c == result[result.size() - 2][1]) {
122 result.erase(result.end() - 2);
123 }
124 }
125
126 return result;
127 }
128
129 // Use Dijkstra-like algorithm to update dual potentials
130 bool refineDual(int src, int dst, vector<int>& dual, vector<pair<int, _Edge*>>& prev) {
131 priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
132 vector<bool> visited(numVertices, false);
133 vector<int> dist(numVertices, INT_MAX);
134
135 pq.emplace(0, src);
136 dist[src] = 0;
137
138 while (!pq.empty()) {
139 auto [currentDist, v] = pq.top();
140 pq.pop();
141
142 if (visited[v]) continue;
143 visited[v] = true;
144
145 if (v == dst) break;
146
147 int dualV = dual[v];
148
149 // Relax edges
150 for (_Edge* edge : graph[v]) {
151 int w = edge->dst;
152
153 if (visited[w] || edge->cap == 0) continue;
154
155 int reducedCost = edge->cost - dual[w] + dualV;
156 int newDist = currentDist + reducedCost;
157
158 if (dist[w] > newDist) {
159 dist[w] = newDist;
160 prev[w] = {v, edge};
161 pq.emplace(newDist, w);
162 }
163 }
164 }
165
166 if (!visited[dst]) return false;
167
168 // Update duals after settling at target
169 int distT = dist[dst];
170 for (int v = 0; v < numVertices; v++) {
171 if (visited[v]) {
172 dual[v] -= distT - dist[v];
173 }
174 }
175
176 return true;
177 }
178};
179
180class Solution {
181public:
182 int findMinimumTime(vector<int>& strength) {
183 int n = strength.size();
184 int source = n * 2;
185 int target = source + 1;
186 MCFGraph graph(target + 1);
187
188 // Add edges from the source to each worker and from each job to the target
189 for (int i = 0; i < n; i++) {
190 graph.addEdge(source, i, 1, 0);
191 graph.addEdge(i + n, target, 1, 0);
192
193 // Add edges from each worker to each job with cost determined by the time taken
194 for (int j = 0; j < n; j++) {
195 graph.addEdge(i, j + n, 1, (strength[i] - 1) / (j + 1) + 1);
196 }
197 }
198
199 // Compute min-cost max flow and return the total minimum cost
200 return graph.flow(source, target, n)[1];
201 }
202};
203
1// Define an edge with source, destination, capacity, flow, and cost
2interface Edge {
3 src: number;
4 dst: number;
5 cap: number;
6 flow: number;
7 cost: number;
8}
9
10// Internal edge representation with a destination, capacity, cost, and reverse edge
11class _Edge {
12 dst: number;
13 cap: number;
14 cost: number;
15 rev: _Edge | null = null;
16
17 constructor(dst: number, cap: number, cost: number) {
18 this.dst = dst;
19 this.cap = cap;
20 this.cost = cost;
21 }
22}
23
24// Define global variables for the number of vertices, graph, and edges
25let numVertices: number;
26let graph: _Edge[][];
27let edges: _Edge[];
28
29// Initialize the graph with a given number of vertices
30function MCFGraph(vertices: number): void {
31 numVertices = vertices;
32 graph = Array.from({ length: numVertices }, () => []);
33 edges = [];
34}
35
36// Add an edge from src to dst with the given capacity and cost
37function addEdge(src: number, dst: number, cap: number, cost: number): number {
38 if (!(0 <= src && src < numVertices) || !(0 <= dst && dst < numVertices) || cap < 0) {
39 throw new Error("Invalid input");
40 }
41
42 const m = edges.length;
43 const edge = new _Edge(dst, cap, cost);
44 const revEdge = new _Edge(src, 0, -cost);
45 edge.rev = revEdge;
46 revEdge.rev = edge;
47
48 graph[src].push(edge);
49 graph[dst].push(revEdge);
50 edges.push(edge);
51 return m;
52}
53
54// Get edge details including source, destination, capacity, and cost
55function getEdge(i: number): Edge {
56 const e = edges[i];
57 const rev = e.rev!;
58 return { src: rev.dst, dst: e.dst, cap: e.cap + rev.cap, flow: rev.cap, cost: e.cost };
59}
60
61// Retrieve a list of all edges in the graph
62function edgesList(): Edge[] {
63 return edges.map((_, i) => getEdge(i));
64}
65
66// Compute the flow from source to target with an optional flow limit
67function flow(src: number, dst: number, flowLimit: number | null): number[] {
68 const result = slope(src, dst, flowLimit);
69 return result[result.length - 1];
70}
71
72// Implementation of the slope algorithm for min-cost max flow
73function slope(src: number, dst: number, flowLimit: number | null): number[][] {
74 if (!(0 <= src && src < numVertices) || !(0 <= dst && dst < numVertices) || src === dst) {
75 throw new Error("Invalid input");
76 }
77
78 // If flow limit is not provided, assume maximum possible flow
79 if (flowLimit === null) {
80 flowLimit = graph[src].reduce((sum, e) => sum + e.cap, 0);
81 }
82
83 const duals = new Array(numVertices).fill(0);
84 const prev: { first: number; second: _Edge }[] = new Array(numVertices).fill(null);
85
86 const result: number[][] = [];
87 result.push([0, 0]);
88
89 while (true) {
90 if (!refineDual(src, dst, duals, prev)) {
91 break;
92 }
93
94 let f = flowLimit;
95 let v = dst;
96
97 // Calculate the path flow
98 while (prev[v] !== null) {
99 const { first: u, second: edge } = prev[v];
100 f = Math.min(f, edge.cap);
101 v = u;
102 }
103
104 // Adjust the flow along the path
105 v = dst;
106 while (prev[v] !== null) {
107 const { first: u, second: edge } = prev[v];
108 edge.cap -= f;
109 edge.rev!.cap += f;
110 v = u;
111 }
112
113 const c = -duals[src];
114 result.push([
115 result[result.length - 1][0] + f,
116 result[result.length - 1][1] + f * c
117 ]);
118
119 if (c === result[result.length - 2][1]) {
120 result.splice(result.length - 2, 1);
121 }
122 }
123
124 return result;
125}
126
127// Use a Dijkstra-like algorithm to update dual potentials
128function refineDual(src: number, dst: number, duals: number[], prev: any[]): boolean {
129 const pq: [number, number][] = []; // Array of tuples [distance, vertex]
130 pq.push([0, src]);
131
132 const visited = new Array(numVertices).fill(false);
133 const distances: (number | null)[] = new Array(numVertices).fill(null);
134 distances[src] = 0;
135
136 while (pq.length > 0) {
137 const [currentDistance, v] = pq.shift()!;
138 if (visited[v]) continue;
139
140 visited[v] = true;
141 if (v === dst) break;
142
143 const dualV = duals[v];
144
145 // Relax edges
146 for (const edge of graph[v]) {
147 const w = edge.dst;
148
149 if (visited[w] || edge.cap === 0) continue;
150
151 const reducedCost = edge.cost - duals[w] + dualV;
152 const newDistance = currentDistance + reducedCost;
153 const currentDistW = distances[w];
154
155 if (currentDistW === null || newDistance < currentDistW) {
156 distances[w] = newDistance;
157 prev[w] = { first: v, second: edge };
158 pq.push([newDistance, w]);
159 }
160 }
161 }
162
163 if (!visited[dst]) return false;
164
165 // Update duals after finding the optimal target
166 const distT = distances[dst]!;
167 for (let v = 0; v < numVertices; v++) {
168 if (visited[v]) {
169 duals[v] -= distT - distances[v]!;
170 }
171 }
172
173 return true;
174}
175
176// Function to compute the minimum time required based on worker strength
177function findMinimumTime(strength: number[]): number {
178 const n = strength.length;
179 const source = n * 2;
180 const target = source + 1;
181 MCFGraph(target + 1);
182
183 // Add edges from the source to each worker and from each job to the target
184 for (let i = 0; i < n; i++) {
185 addEdge(source, i, 1, 0);
186 addEdge(i + n, target, 1, 0);
187
188 // Add edges from each worker to each job with cost determined by the time taken
189 for (let j = 0; j < n; j++) {
190 addEdge(i, j + n, 1, Math.floor((strength[i] - 1) / (j + 1)) + 1);
191 }
192 }
193
194 // Compute min-cost max flow and return the total minimum cost
195 return flow(source, target, n)[1];
196}
197
Time and Space Complexity
The MCFGraph
class implements a minimum-cost flow algorithm, most likely using a form of successive shortest path algorithm, commonly represented as the cycle-canceling algorithm or the successive shortest paths algorithm. The time complexity of this class depends heavily on the number of vertices (n
), the number of edges (m
), and the maximum flow value (U
).
Time Complexity
In each iteration of increasing flow, the algorithm performs Dijkstra's algorithm on the graph, which takes O(m + n \log n)
with a priority queue. Given that flow is incremented one unit at a time, and depending on how many times flow augmentations are needed, the upper bound on the number of augmentations can be O(nU)
, where U
is the sum of capacities. Thus, in the worst case, the overall time complexity can be roughly approximated as:
O(mnU + n^2U \log n)
Specific for the findMinimumTime
problem:
n = number of tasks
- Each task pair introduces up to
n
additional edges in the worst case. - Thus,
m = O(n^2)
and capacities are 1, makingU = n
.
Resulting time complexity becomes:
O(n * n^2 + n^2 * n \log n) = O(n^3 + n^3 \log n) = O(n^3 \log n)
Space Complexity
The space complexity involves the storage of the graph's adjacency list and other auxiliary data for n
vertices and m
edges:
- Graph adjacency list storage requires
O(m)
, wherem = O(n^2)
due to the way edges are added. - Auxiliary data structures like the dual, prev, and distance arrays add up to
O(n)
space each.
Thus, the overall space complexity for the class is:
O(n^2)
For the particular findMinimumTime
scenario, with the given logic, these factors determine the computational complexities.
Learn more about how to find time and space complexity quickly.
Which data structure is used in a depth first search?
Recommended Readings
https algomonster s3 us east 2 amazonaws com cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS
https algomonster s3 us east 2 amazonaws com cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!