NeFut Logo NeFut
Admin Login

Deep Dive into Dynamic Programming and Directed Acyclic Graphs: Algorithm Design Driven by Topological Sequences

Published at: 2026-05-27 09:17 Last updated: 2026-06-06 13:04
#algorithm #DP #DAG

Core Logic and Mathematical Principles

Directed Acyclic Graphs (DAGs) serve as a natural topological framework for Dynamic Programming (DP). Any DP problem that involves state transition equations can essentially be abstracted as a DAG, provided that there are no cyclic dependencies between states.

Running dynamic programming on a DAG (commonly referred to as Topological Sequence Driven DAG-DP) hinges on a core algebraic principle: the one-dimensional linear sequence of topological sorting strictly adheres to the "no aftereffect" constraint required by dynamic programming.

1. Topological Order Ensures Familiarization of State Space

Let the computation of state $v$ depend on predecessor states $u_1, u_2, ext{...}, u_k$. In the DAG topological graph, this is represented by the existence of directed edges $(u_i, v)$.

2. Two Topological Trigger Tactics for State Transition

In practical coding implementations, there are two classic topological-driven tactics for DAG-DP:


Algorithm Derivation and State Design

Taking the classic problem of longest path and scheme count statistics in a directed acyclic graph as an example for state design.

1. State Definition

2. Topological Incremental State Transition Equation (Contribution Method)

When a zero in-degree node $u$ is dequeued from the topological queue, its states $dp[u]$ and $cnt[u]$ are fully familiarized. Traverse each outgoing edge $(u, v)$ starting from $u$, with edge weight $w$:

  1. Longest Path Relaxation (Discovering a Better Topological Path):
    If $dp[u] + w > dp[v]$, it indicates that passing through $u$ can provide a longer path to $v$.

$$dp[v] = dp[u] + w$$

$$cnt[v] = cnt[u] \\ ( ext{the scheme count is entirely inherited from } u)$$

  1. Scheme Count Accumulation (Discovering Parallel Optimal Topological Paths):
    If $dp[u] + w == dp[v]$, it indicates that another optimal path to $v$ of the same length has been found.

$$cnt[v] = (cnt[v] + cnt[u]) \\ ext{mod } MOD$$


C++ Standard Source Code (Topological Sequence Driven DAG-DP Standard Template)

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

const int MAXN = 100005;
const long long MOD = 1000000007LL;
const long long INF = 0x3f3f3f3f3f3f3f3fLL;

struct Edge {
    int to;
    long long weight;
};

std::vector<Edge> adj[MAXN];
int in_degree[MAXN];
int out_degree[MAXN];

long long dp[MAXN];  // dp[i] represents the longest path to node i
long long cnt[MAXN]; // cnt[i] represents the number of schemes to reach node i under the longest path condition

void dag_dp(int n) {
    std::queue<int> q;

    // 1. Initialize physical boundaries: Treat all nodes with an in-degree of 0 (topological sources) as the base for DP
    for (int i = 1; i <= n; ++i) {
        if (in_degree[i] == 0) {
            q.push(i);
            dp[i] = 0;   // Initial longest path for the starting point is 0
            cnt[i] = 1;  // Initial scheme count for the starting point is 1
        } else {
            dp[i] = -INF; // Other dangling points are initialized to negative infinity to prevent illegal transitions
            cnt[i] = 0;
        }
    }

    // 2. Topology-driven execution of DP transitions
    while (!q.empty()) {
        int u = q.front();
        q.pop();

        // Traverse all outgoing edges from u to update subsequent node v (Push-based/Contribution method)
        for (size_t i = 0; i < adj[u].size(); ++i) {
            int v = adj[u][i].to;
            long long w = adj[u][i].weight;

            // Filter out invalid nodes with un-familiarized state (if the starting point is unreachable, skip relaxation)
            if (dp[u] == -INF) continue; 

            // Branch A: Discover a longer path, reset dp and scheme count
            if (dp[u] + w > dp[v]) {
                dp[v] = dp[u] + w;
                cnt[v] = cnt[u];
            }
            // Branch B: Discover a parallel optimal path, accumulate scheme count
            else if (dp[u] + w == dp[v]) {
                cnt[v] = (cnt[v] + cnt[u]) % MOD;
            }

            // Maintain the topological queue
            in_degree[v]--;
            if (in_degree[v] == 0) {
                q.push(v);
            }
        }
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    if (!(std::cin >> n >> m)) return 0;

    for (int i = 0; i < m; ++i) {
        int u, v;
        long long w;
        std::cin >> u >> v >> w;
        adj[u].push_back(Edge{v, w});
        in_degree[v]++;
        out_degree[u]++;
    }

    dag_dp(n);

    // 3. Global extreme value summary
    // Count the optimal states of all "sink nodes with out-degree 0"
    long long max_path_len = -INF;
    for (int i = 1; i <= n; ++i) {
        if (out_degree[i] == 0) {
            max_path_len = std::max(max_path_len, dp[i]);
        }
    }

    long long total_schemes = 0;
    for (int i = 1; i <= n; ++i) {
        if (out_degree[i] == 0 && dp[i] == max_path_len) {
            total_schemes = (total_schemes + cnt[i]) % MOD;
        }
    }

    if (max_path_len < 0) {
        std::cout << "No valid path\n";
    } else {
        std::cout << max_path_len << " " << total_schemes << "\n";
    }

    return 0;
}

NOIP Practical Pitfall Guide

  1. Failure to Distinguish "Topological Sources of the Graph" from "Logically Specified Starting Points in the Problem":
    If the problem explicitly specifies that the path must start from a certain fixed point $S$ during runtime, many contestants tend to initialize all nodes with in_degree == 0 as dp = 0, cnt = 1. This can lead to the inclusion of other zero in-degree sources that should not be counted, resulting in an inflated count of schemes and path lengths compared to the actual values.
  1. Failure to Intercept Invalid Abandoned Nodes with -INF During State Transition:
    As mentioned, if other zero in-degree points that are not the specified starting point remain as -INF, when they are dequeued, if the code does not include the condition if (dp[u] == -INF) continue;, directly executing dp[u] + w may cause negative overflow and erroneously relax and pollute the valid states of other nodes.

Classic NOIP/Luogu Problems

1. Luogu P1137 Travel Plan

2. Luogu P1954 [NOI2010] Air Traffic Control

  1. Step One: Build the reverse graph, changing the original $A \to B$ to $B \to A$. Transform the plane's latest takeoff limits into constraints on the reverse graph.
  2. Step Two: Use a max heap (priority queue) to maintain the currently zero in-degree planes. Each time a plane takes off, we greedily choose the one with the largest latest takeoff limit $k_i$ to place at the end of the reverse sequence. This way, more "tolerance" can be reserved for those planes with stricter constraints later.
  3. Step Three (Finding the Earliest Position for Plane $X$): To allow $X$ to be as forward as possible in the reverse topology, we need to keep it as far back as possible. We can directly avoid letting $X$ enter the takeoff queue during the reverse topological iteration until the queue is empty and the algorithm can no longer proceed without scheduling $X$. At this point, the freed reverse position is essentially the earliest time that $X$ can be blocked in the reverse graph. After integer translation, this will correspond to the earliest takeoff position in the original graph. This entire solution perfectly suppresses the topological dependencies of the DAG.
Original Source: local://12.2

[h] Back to Home