If a DP’s transition from the previous state is linear in a semiring (, , , ), then the DP is a product of small matrices, and a segment tree over the matrices supports point changes to the input in . On trees, split off the heavy child: the transition from the heavy child is a matrix that depends on the light children’s results, and HLD plus one segment tree per heavy path supports vertex updates in .

On sequences

Write where depends only on . Then , and a segment tree storing products (in the right order) answers range queries and point updates.

Example: maximum weight independent set on an array with point updates. State = best with not taken / taken:

Maximum subarray sum with updates is a matrix per element (states: not started, inside, finished). Equivalent and often simpler: store in each node the answers for every combination of boundary states (e.g. best[first taken?][last taken?]), which is the same thing as storing the matrix.

On trees: HLD + matrices

Maximum weight independent set with vertex weight updates. With = heavy child of and = contribution of the light children:

The of the top of a heavy path is the product of the path’s matrices times (the vector of an empty child). Updating changes , hence one leaf of ‘s path tree; the new top value changes of the top’s parent, and so on up light edges.

struct DynamicTreeMIS {                                    // max weight independent set, point weight updates
    static constexpr ll NEG = LLONG_MIN / 4;
    struct Mat { ll a[2][2]; };
    static Mat mul(const Mat& x, const Mat& y) {
        Mat r; for (int i = 0; i < 2; i++) for (int j = 0; j < 2; j++) r.a[i][j] = max({NEG, x.a[i][0] + y.a[0][j], x.a[i][1] + y.a[1][j]});
        return r;
    }
    int n; vector<vector<int>> adj; vector<ll> w;
    vector<int> par, heavy, top, pos, endPos, siz, who; vector<array<ll, 2>> g; vector<Mat> t;
    DynamicTreeMIS(const vector<vector<int>>& tree, const vector<ll>& weights) : n(sz(tree)), adj(tree), w(weights),
        par(n, -1), heavy(n, -1), top(n), pos(n), endPos(n), siz(n, 1), who(n), g(n), t(4 * max(n, 1)) {
        vector<int> order{0};
        for (int i = 0; i < sz(order); i++) for (int v : adj[order[i]]) if (v != par[order[i]]) par[v] = order[i], order.push_back(v);
        for (int i = n - 1; i > 0; i--) siz[par[order[i]]] += siz[order[i]];
        for (int u : order) for (int v : adj[u]) if (v != par[u] && (heavy[u] == -1 || siz[v] > siz[heavy[u]])) heavy[u] = v;
        int cur = 0;
        for (int u : order) {
            if (par[u] != -1 && heavy[par[u]] == u) continue;
            for (int v = u; v != -1; v = heavy[v]) top[v] = u, pos[v] = cur, who[cur++] = v;
            for (int v = u; v != -1; v = heavy[v]) endPos[v] = cur - 1;
        }
        vector<array<ll, 2>> f(n);
        for (int i = n - 1; i >= 0; i--) {
            int u = order[i]; g[u] = {0, w[u]};
            for (int v : adj[u]) if (v != par[u] && v != heavy[u]) g[u][0] += max(f[v][0], f[v][1]), g[u][1] += f[v][0];
            f[u] = g[u];
            if (int h = heavy[u]; h != -1) f[u] = {g[u][0] + max(f[h][0], f[h][1]), g[u][1] + f[h][0]};
        }
        if (n) build(1, 0, n - 1);
    }
    Mat leaf(int v) const { return Mat{{{g[v][0], g[v][0]}, {g[v][1], NEG}}}; }
    void build(int p, int l, int r) {
        if (l == r) { t[p] = leaf(who[l]); return; }
        int m = (l + r) / 2; build(2 * p, l, m); build(2 * p + 1, m + 1, r); t[p] = mul(t[2 * p], t[2 * p + 1]);
    }
    void modify(int p, int l, int r, int i) {
        if (l == r) { t[p] = leaf(who[l]); return; }
        int m = (l + r) / 2; i <= m ? modify(2 * p, l, m, i) : modify(2 * p + 1, m + 1, r, i); t[p] = mul(t[2 * p], t[2 * p + 1]);
    }
    Mat query(int p, int l, int r, int x, int y) const {
        if (x <= l && r <= y) return t[p];
        int m = (l + r) / 2;
        if (y <= m) return query(2 * p, l, m, x, y);
        if (x > m) return query(2 * p + 1, m + 1, r, x, y);
        return mul(query(2 * p, l, m, x, y), query(2 * p + 1, m + 1, r, x, y));
    }
    array<ll, 2> chain(int tp) const { Mat P = query(1, 0, n - 1, pos[tp], endPos[tp]); return {P.a[0][0], P.a[1][0]}; }
    void update(int v, ll x) {
        g[v][1] += x - w[v]; w[v] = x;
        while (v != -1) {
            int tp = top[v]; auto before = chain(tp);
            modify(1, 0, n - 1, pos[v]);
            auto after = chain(tp); int p = par[tp];
            if (p == -1) break;
            g[p][0] += max(after[0], after[1]) - max(before[0], before[1]);
            g[p][1] += after[0] - before[0];
            v = p;
        }
    }
    ll query() const { if (!n) return 0; auto f = chain(0); return max(f[0], f[1]); }
};

Variants

  • Any tree DP with a constant-size state whose dependence on one child is linear in a semiring: max matching, min vertex cover, “number of ways”, path-length DPs with bounded lengths. Build from the same way.
  • per update: replace the per-path segment trees by a globally balanced binary tree (each heavy path’s binary tree is weighted by light subtree sizes), or use a link-cut tree / top tree (Link-Cut Tree, Top Trees).
  • Queries on subtrees or paths instead of the whole tree: query the corresponding range of the heavy path containing the subtree root.
  • Offline alternative: when updates are few and queries many, recompute; when the structure allows deletion-free processing, segment tree over time with rollback.