Off-by-one on range boundaries
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Break down a hard problem into reliable checkpoints, edge-case handling, and complexity trade-offs.
You are given a Directed Acyclic Graph (DAG) with n nodes labeled from 0 to n - 1, represented by a 2D array edges, where edges[i] = [ui, vi] indicates a directed edge from node ui to vi. Each node has an associated score given in an array score, where score[i] represents the score of node i.
You must process the nodes in a valid topological order. Each node is assigned a 1-based position in the processing order.
The profit is calculated by summing up the product of each node's score and its position in the ordering.
Return the maximum possible profit achievable with an optimal topological order.
A topological order of a DAG is a linear ordering of its nodes such that for every directed edge u → v, node u comes before v in the ordering.
Example 1:
Input: n = 2, edges = [[0,1]], score = [2,3]
Output: 8
Explanation:
Node 1 depends on node 0, so a valid order is [0, 1].
| Node | Processing Order | Score | Multiplier | Profit Calculation |
|---|---|---|---|---|
| 0 | 1st | 2 | 1 | 2 × 1 = 2 |
| 1 | 2nd | 3 | 2 | 3 × 2 = 6 |
The maximum total profit achievable over all valid topological orders is 2 + 6 = 8.
Example 2:
Input: n = 3, edges = [[0,1],[0,2]], score = [1,6,3]
Output: 25
Explanation:
Nodes 1 and 2 depend on node 0, so the most optimal valid order is [0, 2, 1].
| Node | Processing Order | Score | Multiplier | Profit Calculation |
|---|---|---|---|---|
| 0 | 1st | 1 | 1 | 1 × 1 = 1 |
| 2 | 2nd | 3 | 2 | 3 × 2 = 6 |
| 1 | 3rd | 6 | 3 | 6 × 3 = 18 |
The maximum total profit achievable over all valid topological orders is 1 + 6 + 18 = 25.
Constraints:
1 <= n == score.length <= 221 <= score[i] <= 1050 <= edges.length <= n * (n - 1) / 2edges[i] == [ui, vi] denotes a directed edge from ui to vi.0 <= ui, vi < nui != viProblem summary: You are given a Directed Acyclic Graph (DAG) with n nodes labeled from 0 to n - 1, represented by a 2D array edges, where edges[i] = [ui, vi] indicates a directed edge from node ui to vi. Each node has an associated score given in an array score, where score[i] represents the score of node i. You must process the nodes in a valid topological order. Each node is assigned a 1-based position in the processing order. The profit is calculated by summing up the product of each node's score and its position in the ordering. Return the maximum possible profit achievable with an optimal topological order. A topological order of a DAG is a linear ordering of its nodes such that for every directed edge u → v, node u comes before v in the ordering.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: Array · Dynamic Programming · Bit Manipulation · Topological Sort
2 [[0,1]] [2,3]
3 [[0,1],[0,2]] [1,6,3]
Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
class Solution {
public int maxProfit(int n, int[][] edges, int[] score) {
final int maxMask = 1 << n;
// need[i] := the bitmask representing all nodes that must be placed before node i
int[] need = new int[n];
// dp[mask] := the maximum profit achievable by placing the set of nodes represented by `mask`
int[] dp = new int[maxMask];
Arrays.fill(dp, -1);
dp[0] = 0;
for (int[] edge : edges) {
final int u = edge[0];
final int v = edge[1];
need[v] |= 1 << u;
}
// Iterate over all subsets of nodes (represented by bitmask `mask`)
for (int mask = 0; mask < maxMask; ++mask) {
if (dp[mask] == -1)
continue;
// Determine the position of the next node to be placed (1-based).
int pos = Integer.bitCount(mask) + 1;
// Try to place each node `i` that hasn't been placed yet.
for (int i = 0; i < n; ++i) {
if ((mask >> i & 1) == 1)
continue;
// Check if all dependencies of node `i` are already placed.
if ((mask & need[i]) == need[i]) {
final int newMask = mask | 1 << i; // Mark node `i` as placed.
dp[newMask] = Math.max(dp[newMask], dp[mask] + score[i] * pos);
}
}
}
return dp[maxMask - 1];
}
}
// Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
package main
import (
"math/bits"
"slices"
)
// https://space.bilibili.com/206214
func maxProfit(n int, edges [][]int, score []int) (ans int) {
if len(edges) == 0 {
slices.Sort(score)
for i, s := range score {
ans += s * (i + 1)
}
return
}
// 记录每个节点的先修课(直接前驱)
pre := make([]int, n)
for _, e := range edges {
pre[e[1]] |= 1 << e[0]
}
memo := make([]int, 1<<n)
var dfs func(s int) int
dfs = func(s int) (res int) {
m := &memo[s]
if *m > 0 {
return *m
}
defer func() { *m = res }()
i := bits.OnesCount(uint(s)) // 已学课程数
// 枚举还没学过的课程 j,且 j 的所有先修课都学完了
for j, p := range pre {
if s>>j&1 == 0 && s|p == s {
res = max(res, dfs(s|1<<j)+score[j]*(i+1))
}
}
return
}
return dfs(0)
}
// 超时
func maxProfit2(n int, edges [][]int, score []int) (ans int) {
if len(edges) == 0 {
slices.Sort(score)
for i, s := range score {
ans += s * (i + 1)
}
return
}
// 记录每个节点的先修课(直接前驱)
pre := make([]int, n)
for _, e := range edges {
pre[e[1]] |= 1 << e[0]
}
u := 1 << n
f := make([]int, u)
for s := u - 2; s >= 0; s-- {
i := bits.OnesCount(uint(s)) // 已学课程数
// 枚举还没学过的课程 j,且 j 的所有先修课都学完了
for j, p := range pre {
if s>>j&1 == 0 && s|p == s {
f[s] = max(f[s], f[s|1<<j]+score[j]*(i+1))
}
}
}
return f[0]
}
func maxProfit3(n int, edges [][]int, score []int) (ans int) {
if len(edges) == 0 {
slices.Sort(score)
for i, s := range score {
ans += s * (i + 1)
}
return
}
// 记录每个节点的先修课(直接前驱)
pre := make([]int, n)
for _, e := range edges {
pre[e[1]] |= 1 << e[0]
}
u := 1 << n
f := make([]int, u)
for s := 1; s < u; s++ {
f[s] = -1
}
for s, fs := range f {
if fs < 0 { // 不合法状态,比如已经学完后面的课程,但前面的课程还没学
continue
}
i := bits.OnesCount(uint(s)) // 已学课程数
// 枚举还没学过的课程 j,且 j 的所有先修课都学完了
for cus, lb := u-1^s, 0; cus > 0; cus ^= lb {
lb = cus & -cus
j := bits.TrailingZeros(uint(lb))
if s|pre[j] == s {
newS := s | lb
f[newS] = max(f[newS], fs+score[j]*(i+1))
}
}
}
return f[u-1]
}
# Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
class Solution:
def maxProfit(self, n: int, edges: list[list[int]], score: list[int]) -> int:
# need[i] := the bitmask representing all nodes that must be placed before
# node i
need = [0] * n
# dp[mask] := the maximum profit achievable by placing the set of nodes
# represented by `mask`
dp = [-1] * (1 << n)
dp[0] = 0
for u, v in edges:
need[v] |= 1 << u
# Iterate over all subsets of nodes (represented by bitmask `mask`)
for mask in range(1 << n):
if dp[mask] == -1:
continue
# Determine the position of the next node to be placed (1-based).
pos = mask.bit_count() + 1
# Try to place each node `i` that hasn't been placed yet.
for i in range(n):
if mask >> i & 1:
continue
# Check if all dependencies of node `i` are already placed.
if (mask & need[i]) == need[i]:
newMask = mask | 1 << i # Mark node `i` as placed.
dp[newMask] = max(dp[newMask], dp[mask] + score[i] * pos)
return dp[-1]
// Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
// Rust example auto-generated from java reference.
// Replace the signature and local types with the exact LeetCode harness for this problem.
impl Solution {
pub fn rust_example() {
// Port the logic from the reference block below.
}
}
// Reference (java):
// // Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
// class Solution {
// public int maxProfit(int n, int[][] edges, int[] score) {
// final int maxMask = 1 << n;
// // need[i] := the bitmask representing all nodes that must be placed before node i
// int[] need = new int[n];
// // dp[mask] := the maximum profit achievable by placing the set of nodes represented by `mask`
// int[] dp = new int[maxMask];
// Arrays.fill(dp, -1);
// dp[0] = 0;
//
// for (int[] edge : edges) {
// final int u = edge[0];
// final int v = edge[1];
// need[v] |= 1 << u;
// }
//
// // Iterate over all subsets of nodes (represented by bitmask `mask`)
// for (int mask = 0; mask < maxMask; ++mask) {
// if (dp[mask] == -1)
// continue;
// // Determine the position of the next node to be placed (1-based).
// int pos = Integer.bitCount(mask) + 1;
// // Try to place each node `i` that hasn't been placed yet.
// for (int i = 0; i < n; ++i) {
// if ((mask >> i & 1) == 1)
// continue;
// // Check if all dependencies of node `i` are already placed.
// if ((mask & need[i]) == need[i]) {
// final int newMask = mask | 1 << i; // Mark node `i` as placed.
// dp[newMask] = Math.max(dp[newMask], dp[mask] + score[i] * pos);
// }
// }
// }
//
// return dp[maxMask - 1];
// }
// }
// Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
// Auto-generated TypeScript example from java.
function exampleSolution(): void {
}
// Reference (java):
// // Accepted solution for LeetCode #3530: Maximum Profit from Valid Topological Order in DAG
// class Solution {
// public int maxProfit(int n, int[][] edges, int[] score) {
// final int maxMask = 1 << n;
// // need[i] := the bitmask representing all nodes that must be placed before node i
// int[] need = new int[n];
// // dp[mask] := the maximum profit achievable by placing the set of nodes represented by `mask`
// int[] dp = new int[maxMask];
// Arrays.fill(dp, -1);
// dp[0] = 0;
//
// for (int[] edge : edges) {
// final int u = edge[0];
// final int v = edge[1];
// need[v] |= 1 << u;
// }
//
// // Iterate over all subsets of nodes (represented by bitmask `mask`)
// for (int mask = 0; mask < maxMask; ++mask) {
// if (dp[mask] == -1)
// continue;
// // Determine the position of the next node to be placed (1-based).
// int pos = Integer.bitCount(mask) + 1;
// // Try to place each node `i` that hasn't been placed yet.
// for (int i = 0; i < n; ++i) {
// if ((mask >> i & 1) == 1)
// continue;
// // Check if all dependencies of node `i` are already placed.
// if ((mask & need[i]) == need[i]) {
// final int newMask = mask | 1 << i; // Mark node `i` as placed.
// dp[newMask] = Math.max(dp[newMask], dp[mask] + score[i] * pos);
// }
// }
// }
//
// return dp[maxMask - 1];
// }
// }
Use this to step through a reusable interview workflow for this problem.
Pure recursion explores every possible choice at each step. With two choices per state (take or skip), the decision tree has 2ⁿ leaves. The recursion stack uses O(n) space. Many subproblems are recomputed exponentially many times.
Each cell in the DP table is computed exactly once from previously solved subproblems. The table dimensions determine both time and space. Look for the state variables — each unique combination of state values is one cell. Often a rolling array can reduce space by one dimension.
Review these before coding to avoid predictable interview regressions.
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Wrong move: An incomplete state merges distinct subproblems and caches incorrect answers.
Usually fails on: Correctness breaks on cases that differ only in hidden state.
Fix: Define state so each unique subproblem maps to one DP cell.