算法与数据结构 12 - 数据结构+动态规划大杂烩

目录

  • 部分运算的规定
  • 数据结构
    • 链表
    • ST 表
      • Method of Four Russians
    • 线段树
    • 字典树 Trie
    • 树状数组
    • 哈希表
    • 分块
  • 动态规划
    • 背包 DP
    • 数位 DP
    • 树形 DP
      • 树上背包
      • 换根 DP
    • 其他 DP 优化
      • 单调栈、单调队列优化 DP
      • 斜率优化 DP
      • 四边形不等式优化 DP(决策单调性优化 DP)
    • DP 套 DP

0. 部分运算的规定

若无特殊说明,所有变量均为整数,整数除法自动向下取整。

本文约定 \(\log x=\log_2x\)

1.1 链表

链表是一种不那么常见的数据结构,通常用于维护需要“在中间插入”的动态信息。它的核心思路是为链表中的每个节点维护指向上一个和下一个元素的指针,通过修改指针的指向进行元素增删。也正因如此,链表的修改速度很快,单点插入、删除的时间复杂度为 \(\mathcal{O}(1)\),但在链表上随机访问的速度极慢,最坏为 \(\mathcal{O}(n)\)

在部分简单的应用场景下,节点可以不存储指向上一个元素的指针,此时的链表叫单向链表,只能向一个方向遍历而不能反向;相应地,将其中节点存储两个方向指针的链表称作双向链表。

插入时:先将待插入节点的两个指针指向插入位置的上一个和下一个节点;再将插入位置的上一个节点的后向指针指向待插入节点;最后将插入位置的下一个节点的前向指针指向待插入节点。

删除时:先将删除节点的上一个节点的后向指针指向待删除节点的下一个节点;再将删除位置的下一个节点的前向指针指向待插入节点的上一个节点;最后清除待删除节点的所有信息。

为了方便编写代码,你可以在创建空链表时一并新建两个空白节点,代表链表的头部和尾部,当然不建立特殊节点也可以。除了插入和删除外,链表还可以以 \(O(1)\) 的时间复杂度处理很多操作,比如将两个链表首尾相接等。

例题P3201 [HNOI2009] 梦幻布丁

给定 \(n\) 个数字 \(a_i\),有 \(2\) 种操作:1. 把 \(=x\) 的所有 \(a_i\) 的值改成 \(y\);2. 问整个序列中的数字段数。一个数字段是极长的一段相同数字。

注:这道题也可以使用可合并线段树或平衡树维护题目要求的信息,但因为过于复杂,此处不予展开。

考虑为每一种数字维护一个集合,每次操作 1 把 \(x\) 的集合和 \(y\) 的集合合并,合并时检查 \(x\) 集合中的每个元素所在位置的两侧是否与它相等,如果有一侧相等则全局数字段数会减 \(1\)(考虑 abc \(\rightarrow\) aac),如果两侧均相等则段数减 \(2\)(考虑 aba \(\rightarrow\) aaa)。如果用链表维护集合,则合并(通过链表拼接实现)的时间复杂度为 \(O(1)\),但总时间复杂度为 \(O(n^2)\),还是太高;此时可以联想到“线段树合并”中的“启发式合并”技巧,即每次把小的集合合并到大的上面。可以证明,这样操作的平均时间复杂度为 \(O(n\log n)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
#define rep(i, s, t) for(ll i=(s);i<=(t);i++)
#define per(i, s, t) for(ll i=(s);i>=(t);i--)
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const ll N = 1000005;
int n, m, a[N], ral[N], siz[N];
struct node {
    int x, nxt;
} l[N];
int head[N], tail[N], tot;
void insert(int x, int y) {
    if (!head[x]) {
        head[x] = tail[x] = ++tot;
        l[tot] = {y, 0};
        siz[x]++;
    } else {
        l[tail[x]].nxt = ++tot;
        l[tot] = {y, 0};
        tail[x] = tot;
        siz[x]++;
    }
}
void merge(int x, int y) {
    l[tail[x]].nxt = head[y];
    tail[x] = tail[y];
    head[y] = tail[y] = siz[y] = 0;
}
int main() {
    rep(i, 1, 1000000) ral[i] = i;
    read(n, m);
    int ans = 0;
    rep(i, 1, n) {
        read(a[i]);
        ans += (a[i] != a[i - 1]);
        insert(a[i], i);
    }
    rep(i, 1, 2) {
        int now = head[i];
        while (now) {
            now = l[now].nxt;
        }
    }
    rep(i, 1, m) {
        int op, x, y;
        read(op);
        if (op == 1) {
            read(x, y);
            if (x == y) continue;
            if (siz[ral[x]] > siz[ral[y]]) {
                swap(ral[x], ral[y]);
            }
            x = ral[x]; y = ral[y];
            if (!siz[x]) continue;
            int now = head[x];
            while (now) {
                ans -= (y == a[l[now].x - 1]);
                ans -= (y == a[l[now].x + 1]);
                siz[x]--;
                siz[y]++;
                now = l[now].nxt;
            }
            now = head[x];
            while (now) {
                a[l[now].x] = y;
                now = l[now].nxt;
            }
            merge(y, x);
        } else {
            printf("%d\n", ans);
        }
    }
    return 0;
}

1.2 ST 表

ST 表,也叫稀疏表,是一种用于解决可重复贡献问题的数据结构。常见的可以使用 ST 表解决的问题:静态区间最值、静态区间 \(\gcd\)、区间按位与等。一般来说,一种运算 \(\oplus\) 满足“可重复贡献”需要满足:

  • \(a\oplus a=a\)
  • \(a\oplus b\oplus c=a\oplus (b\oplus c)\)

ST 表基于倍增思想,简单来说,令 \(f_{i,j}\) 表示 \(\bigoplus\limits_{k=i}\limits^{i+2^j-1}a_k\),则有 \(\bigoplus\limits_{l\le i\le r}a_i=f_{l,k}\oplus f_{r - 2^k+1,k}\),其中 \(2^{k}\le r-l+1\le 2^{k+1}\)。容易发现,\(f\) 可以在 \(O(n \log n)\) 的时间复杂度内预处理完成,单次查询的时间复杂度为 \(O(1)\)\(O(\log n)\),取决于求 \(k\) 的方法。预处理 \(\log i\) 可以实现 \(O(1)\)\(k\)

1.2.1 Method of Four Russians

Method of Four Russians,没有正式的中文译名,但有时被称为“四毛子算法”,通常用于解决“区间最大/最小值”(RMQ)的问题。

遗憾的是,据盖金社报道,这四个毛子中的三个——维亚切斯拉夫·巴兰尼科夫‌、安东·尤丁采夫和尼基塔·布亚诺夫三人已于日前乘坐俄制 BMPT“终结者”支援战车时因弹药殉爆不幸遇难。(本段内容是胡扯的)

简单来说,考虑将长度为 \(n\) 的原序列划分为 \(S=\log n\) 个块,为每个长为 \(\frac{n}{S}\) 的块计算其最值,为每个块建立一个 ST 表,再为每个块的最值从前到后拼接成的序列建立一个 ST 表。查询 \([l,r]\) 的最值时,只需要分别对 \(l\) 所在块、\(r\) 所在块的 ST 表分别查询一次、再在最值 ST 表上查第三次就可以了。预处理时间复杂度 \(O(n\log\log n)\)。但常数比较大,用的场合比较小。

例题P5502 [JSOI2015] 最大公约数

给定长为 \(n\) 的数组 \(a\),求一个子串 \(a[l\cdots r]\) 使得 \((r-l+1)\times\gcd\limits_{l\le i\le r}a[i]\) 最大。

枚举 \(r\),考虑每种可能的 \(\gcd\),二分或倍增计算对应的 \(l\),然后更新答案。由于 \(\gcd\) 加入一个数后要么不变,要么降为原来的 \(\frac12\) 及以下,因此总的 \(\gcd\) 可能数为 \(\log a_{max}\) 种。区间 \(\gcd\) 可以用 ST 表维护,\(O(\log)\) 时间查询。时间复杂度应为 \(O(n\log n\log a_{max})\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const int N = 100005;
int n; ll a[N], st[N][21], pow2[21], Log2[N];
ll query(int l, int r) {
    int K = Log2[r - l + 1];
    return __gcd(st[l][K], st[r - pow2[K] + 1][K]);
}
void pd() {
    pow2[0] = 1;
    rep(i, 1, 20) pow2[i] = pow2[i - 1] * 2;
    Log2[0] = LLONG_MIN; Log2[1] = 0;
    rep(i, 2, N - 1) Log2[i] = Log2[i >> 1] + 1;
    read(n);
    rep(i, 1, n) read(a[i]);
    rep(i, 1, n) st[i][0] = a[i];
    rep(j, 1, 20) {
        rep(i, 1, n - pow2[j] + 1) st[i][j] = __gcd(st[i][j - 1], st[i + pow2[j - 1]][j - 1]);
    }
    ll ans = 0;
    rep(r, 1, n) {
        int l = r;
        ans = max(ans, a[r]);
        while (l >= 1) {
            ll now = query(l, r);
            for (int i = Log2[n]; i >= 0; i--) {
                if (l - pow2[i] >= 1 && query(l - pow2[i], r) == now) l -= pow2[i];
            }
            ans = max(ans, now * (r - l + 1));
            l--;
        }
    }
    cout << ans << '\n';
}
int main() {
    pd();
    return 0;
}

1.3 线段树

线段树通过递归的形式维护区间数据,可以应用在动态规划的优化、维护各类数据等方面。

线段树由若干个节点组成,每个节点有两个儿子指针,指向自己的儿子,除了叶子节点以外。一般来说,如果一个节点维护的区间是 \([l,r]\),则左儿子维护 \([l, mid]\),右儿子维护 \([mid + 1, r]\)

当需要对 \([L, R]\) 区间内的所有数据进行相同操作(比如同时加一个数),则会从 \([1,n]\) 节点开始,对于每个与 \([L,R]\) 有交的 \([l,r]\),都更新一遍数据。为了降低时间复杂度,往往在修改后不会立刻修改所有节点,而是只修改最顶层的节点 \([1,n]\),并在这个节点上打上“标记”。当需要查询某个节点儿子的数据时,如果这个点在之前被打上标记,则根据标记内容修改自己的儿子,在儿子上打上相应的标记,再将自己的标记去除,这种操作被成为“标记下传”。这样,由于线段树二叉树的结构,单次查询、修改的平均时间复杂度为 \(O(n \log n)\),但因为需要递归,常数会比较大。因此,如果某种数据结构的实现可以不适用线段树,一般不建议(平衡树除外,这个常数更大)。

例题P14301 [JOI2023 预选赛 R2] 日本沈没 2

日本包含 \(n\) 座岛屿,自西向东第 \(i\) 座岛屿海拔为 \(a_i\)。在未来的 \(q\) 天内会发生以下事件之一:1. 刮强度为 \(x\) 的西风;2. 刮强度为 \(x\) 的东风;3. 查询第 \(i\) 座岛的海拔。其中,刮强度为 \(x\) 的 y 风会使从 y 数起的 \(x\) 座岛中,所有 y 方向没有比自己高的岛屿的岛屿的海拔都会被刮掉 \(1\) m。

以下的 \(a_i\) 始终为初始值。考虑设 \(f_i=\min(premax_i, sufmax_i)\),则 \(f\) 为单峰函数(有平台)。加入在某个时刻,\(a_i\ge f_i\),则说明 \(i\) 的海拔未曾改变,因为 \(i\) 的两侧有比它高的岛;反之,则第 \(i\) 座岛的真实海拔为 \(f_i\),因为当 \(i\) 的一侧不再有比它高的岛时,从这一侧刮来的风会让它和这一侧曾经比它高的岛一起被刮掉高度。因此,在任意时刻,第 \(i\) 座岛屿的真实海拔是 \(\min(a_i, f_i)\)

考虑如何维护 \(f_i\)。可以看出,\(f_i\) 的变化一定是连续的,因此可以用线段树维护。现在考虑每次修改后 \(f_i\) 的变化区间。下面以强度为 \(x\) 的西风为例:设此时最靠右的 \(f_i\) 最大值为 \(f_j\),如果 \(x\ge j\),则 \([1,j]\)\(f\)\(-1\),而 \([j+1,x]\)\(f\) 因为有 \(j\) 的存在而不会减小;反之,如果 \(x<j\),则需要找到 \(x\) 东侧第一个 \(a_i\ge f_i\) 的位置 \(k\),这是因为 \([1,x]\)\(f\) 会因为刮风而 \(-1\)\([x+1,k]\) 因为这个区间内的 \(premax\)\([1,x]\) 所以 \(f\) 会跟着一起 \(-1\),而再靠东边的 \(f\) 因为 \(k\) 的存在就不受影响了。如果刮东风,则按相反方向处理即可。因此,线段树需要维护:1. 区间 \(f\) 最大值;2. 区间 \(f_i-a_i\) 最小值。上述的 \(j,k\) 以及它们反方向的 \(j',k'\) 可以二分计算,因此时间复杂度为 \(O(n\log^2 n)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const int N = 300005;
int n, m, a[N], c[N];
int f[N << 2], mx[N << 2], lazy[N << 2], les[N << 2];
void pushup(int rt) {
    mx[rt] = max(mx[rt << 1], mx[rt << 1 | 1]);
    les[rt] = min(les[rt << 1], les[rt << 1 | 1]);
}
void pushdown(int rt) {
    if (lazy[rt]) {
        mx[rt << 1] -= lazy[rt];
        mx[rt << 1 | 1] -= lazy[rt];
        les[rt << 1] -= lazy[rt];
        les[rt << 1 | 1] -= lazy[rt];
        lazy[rt << 1] += lazy[rt];
        lazy[rt << 1 | 1] += lazy[rt];
        lazy[rt] = 0;
    }
}
void update(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) {
        mx[rt]--;
        les[rt]--;
        lazy[rt]++;
        return;
    }
    pushdown(rt);
    int mid = (l + r) >> 1;
    if (L <= mid) update(rt << 1, l, mid, L, R);
    if (R > mid) update(rt << 1 | 1, mid + 1, r, L, R);
    pushup(rt);
}
int querymx(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return mx[rt];
    pushdown(rt);
    int mid = (l + r) >> 1;
    int res = INT_MIN;
    if (L <= mid) res = max(res, querymx(rt << 1, l, mid, L, R));
    if (R > mid) res = max(res, querymx(rt << 1 | 1, mid + 1, r, L, R));
    return res;
}
int querymn(int rt, int l, int r, int L, int R) {
    if (L <= l && r <= R) return les[rt];
    pushdown(rt);
    int mid = (l + r) >> 1;
    int res = INT_MAX;
    if (L <= mid) res = min(res, querymn(rt << 1, l, mid, L, R));
    if (R > mid) res = min(res, querymn(rt << 1 | 1, mid + 1, r, L, R));
    return res;
}
void build(int rt, int l, int r) {
    if (l == r) {
        mx[rt] = f[l];
        les[rt] = f[l] - a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(rt << 1, l, mid);
    build(rt << 1 | 1, mid + 1, r);
    pushup(rt);
}
stack<pii> q;
int pre[N], suf[N];
int maxn, maxl, maxr;
int main() {
    read(n, m);
    rep(i, 1, n) {
        read(a[i]);
        maxn = max(maxn, a[i]);
    }
    rep(i, 1, n) {
        if (a[i] == maxn) {
            if (!maxl) maxl = i;
            maxr = i;
        }
    }
    rep(i, 1, n) pre[i] = max(pre[i - 1], a[i]);
    per(i, n, 1) suf[i] = max(suf[i + 1], a[i]);
    rep(i, 1, n) f[i] = min(pre[i], suf[i]);
    build(1, 1, n);
    rep(i, 1, m) {
        int op, x;
        read(op, x);
        if (op == 1) {
            if (x >= maxr) {
                update(1, 1, n, 1, maxr);
            } else {
                int l = x + 1, r = maxr, tmp = -1;
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (querymn(1, 1, n, x + 1, mid) <= 0) {
                        r = mid - 1, tmp = mid;
                    } else l = mid + 1;
                }
                if (tmp - 1 >= 1) update(1, 1, n, 1, tmp - 1);
            }
        } else if (op == 2) {
            if (n - x + 1 <= maxl) {
                update(1, 1, n, maxl, n);
            } else {
                int l = maxl, r = n - x, tmp = -1;
                while (l <= r) {
                    int mid = (l + r + 1) >> 1;
                    if (querymn(1, 1, n, mid, n - x) <= 0) {
                        l = mid + 1, tmp = mid;
                    } else r = mid - 1;
                }
                if (tmp + 1 <= n) update(1, 1, n, tmp + 1, n);
            }
        } else {
            printf("%d\n", min(querymx(1, 1, n, x, x), a[x]));
        }
        int l = 1, r = n, now = querymx(1, 1, n, 1, n);
        while (l < r) {
            int mid = (l + r) >> 1;
            if (querymx(1, 1, n, 1, mid) == now) r = mid;
            else l = mid + 1; 
        }
        maxl = r;
        l = 1, r = n;
        while (l < r) {
            int mid = (l + r + 1) >> 1;
            if (querymx(1, 1, n, mid, n) == now) l = m  id;
            else r = mid - 1; 
        }
        maxr = l;
    }
    return 0;
}

1.3.1 单侧递归

当线段树维护无法 \(O(1)\) 合并两个儿子的信息时,就需要使用单侧递归技巧。这个技巧的目的是在 \(O(\log n)\) 的时间复杂度内对一个节点进行 pushup。简单来说,如果两个儿子需要合并某个信息,而这个信息需要访问节点的子树才能知道,则想办法在访问子树时,在节点提前存好一些东西,使得访问子树时只需要访问一个儿子而不用访问另一个。

例题P4198 楼房重建

\(n\) 个数的序列 \(a\) 初始全是 \(0\)\(m\) 次操作,每次修改 \(x_i\)\(y_i\),求每次操作后能在 \((0,0)\)“看到”(不存在 \(j<i\) 使得 \(\frac{a_j}j\ge \frac{a_i}i\))的 \(i\) 数量。

此题相当于维护斜率 \(\frac{a_i}i\),而后算符合要求的序列长度:第一个必选;后面的若大于前一个选的则选,反之不选。可以想到用线段树维护,此时线段树的节点维护 \([l,r]\) 中,若选择 \(\frac{a_l}l\) 的序列长度,此时左右儿子不能直接合并,于是采用上述技巧解决。时间复杂度因为 pushup 操作多一个 \(\log\),为 \(O(n\log^2 n)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const int N = 100005;
int n, m;
double h[N];
double mx[N << 2];
int len[N << 2];
int getlen(int rt, int l, int r, double x) {
    if (mx[rt] <= x) return 0;
    if (l == r) return 1;
    int mid = (l + r) >> 1;
    if (mx[rt << 1] <= x) return getlen(rt << 1 | 1, mid + 1, r, x);
    else return len[rt] - len[rt << 1] + getlen(rt << 1, l, mid, x);
}
void pushup(int rt, int l, int r) {
    int mid = (l + r) >> 1;
    mx[rt] = max(mx[rt << 1], mx[rt << 1 | 1]);
    len[rt] = len[rt << 1] + getlen(rt << 1 | 1, mid + 1, r, mx[rt << 1]);
}
void update(int rt, int l, int r, int x, double y) {
    if (l == r) {
        len[rt] = 1;
        mx[rt] = y;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) update(rt << 1, l, mid, x, y);
    else update(rt << 1 | 1, mid + 1, r, x, y);
    pushup(rt, l, r);
}
int main() {
    read(n, m);
    rep(i, 1, m) {
        int x, y;
        read(x, y);
        h[x] = (double)y / x;
        update(1, 1, n, x, h[x]);
        printf("%d\n", len[1]);
    }
    return 0;
}

1.3.2 线段树二分

比二分套线段树少一个 \(\log\)。在“主席树”一章展开叙述。

1.4 字典树(Trie)

字典树是一种外向树形结构,每个节点有若干个指针从它出发,指向其他节点。从起点出发的、由若干条指针首尾相连而成的路径代表一个字符串,每条指针代表一个字符。

例题P4407 [JSOI2009] 电子字典

给定若干模板串和若干询问,求询问串是否与某个模板串相同,询问串与多少个模板串的编辑距离为 \(1\)

考虑在模板串建成的 Trie 上 DFS,DFS 有三个状态:当前节点、当前字符串上字符位置、是否被编辑过。三种编辑方式(删除、增添、修改)分别对应 Trie 上 DFS 的三种操作:跳过字符串上字符(位置 \(+1\))、移动至另一节点(节点 \(\leftarrow\) 某一儿子)、同时进行上面两种操作。注意去重,比如对于 abbc,删除第二个字符和第三个字符是等价的,应只选取一种情况继续 DFS。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const int N = 500005;
int n, m, tot = 1;
pii seg[N];
struct node {
    int son[26], cnt;
} trie[N];
int len;
void insert(char *s, int len) {
    int now = 1;
    rep(i, 0, len - 1) {
        char c = s[i];
        if (!trie[now].son[c - 'a']) trie[now].son[c - 'a'] = ++tot;
        now = trie[now].son[c - 'a'];
    }
    trie[now].cnt++;
}
int ans = 0;
char s[30];
void dfs(int now, int pos, bool edited) {
    if (ans == -1) return;
    if (pos >= len) {
        if (!edited) {
            if (trie[now].cnt) ans = -1;
            else {
                rep(i, 0, 25) if (trie[now].son[i]) dfs(trie[now].son[i], pos, 1);
            }
        }
        else if (trie[now].cnt) ans++;
        return;
    }
    if (trie[now].son[s[pos] - 'a']) {
        dfs(trie[now].son[s[pos] - 'a'], pos + 1, edited);
    }
    if (!edited) {
        if (pos + 1 == len || s[pos + 1] != s[pos]) dfs(now, pos + 1, 1);
        rep(i, 0, 25) {
            if (trie[now].son[i]) {
                if (i + 'a' != s[pos]) dfs(trie[now].son[i], pos, 1);
                if (i + 'a' != s[pos]) dfs(trie[now].son[i], pos + 1, 1);
            }
        }
    }
}
int main() {
    read(n, m);
    rep(i, 1, n) {
        scanf("%s", s);
        insert(s, strlen(s));
    }
    rep(i, 1, m) {
        scanf("%s", s);
        len = strlen(s);
        ans = 0;
        dfs(1, 0, 0);
        printf("%d\n", ans);
    }
    return 0;
}

1.5 树状数组(BIT)

树状数组者,行“单修”“区查”之数据结构也。因数组之拆分少于 \(\log n\),颇似二叉树之结构,故曰“树状数组”云云。欲行是术,需览信息之律,合“结合律”而“可差分”,方能行此之法。

何谓“结合律”“可差分”?有三数 \(x,y,z\) 与一法 \(\oplus\),合 \((x\oplus y)\oplus z=x\oplus(y\oplus z)\) 之律,虽结合任易可也,故曰“结合律”;亦有三数 \(x,y,z\) 与一法 \(\oplus\),有 \(x\oplus y=z\),隐 \(y\) 而仍以 \(x,z\) 知之,以差而分明,故谓之曰“可差分”。

由此观之,若一法可以线段树行之,树状数组亦可也;线段树可行之法,恨树状数组弗能为。然树状数组可以 \(15\) 行 C++ 为之,且其速快甚,线段树莫能与之争。

树状数组以 lowbit 函数为之。欲使单修、区查之时间为 \(O(\log n)\),故假不多于 \(\log n\) 之区间,管辖任意 \(a[l,r]\),以竟厥功,爰由 \(s[i]\)\(a[i-lowbit+1\cdots i]\),俾克减损其时。

#define lowbit(x) ((x)&-(x))
int lowbit(int x) {return x & -x;}

此 lowbit 之 \(O(1)\) 算法。lowbit(x) 实为 \(x\) 最低 \(1\) 与更低 \(0\) 之组合。若使 \(s_i=\bigoplus\limits_{j=i-lowbit(i)+1}\limits^i a_j\),则 \(s_i\)\(a_i\) 之树状数组,用是查 \(\bigoplus\limits_{i=1}^r a_i\),故需差分,以查 \(a[l\cdots r]\)

为求上文所述单修、区查之术,需改、览含 \(a_i\) 之区间全。易知,\(s_i\) 必含 \(a_i\)。若 \(s_p\)\(a_i\),而 \(lowbit(p+lowbit(p))\ge 2\times lowbit(p)\),故 \(s_{p+lowbit(p)}\) 必含 \(a_i\),反之亦然。故有

void add(int x, int k) {
	while (x <= n) {
		tree[x] ⊕= k;
		x += lowbit(x);
	}
}
int sum(int x) {
	int ans = IDENTITY_ELEMENT;
	while (x != 0) {
		ans ⊕= tree[x];
		x -= lowbit(x);
	}
	return ans;
}

之码。

例题P10798 「CZOI-R1」消除威胁

今有数 \(n\),聚列 \(A\)。可任意易其正负,易 \(A_i\)​ 为 \(−A_i\)​。若区间 \([l,r]\) 中,\(l<r\),两端之值相等,且其中诸数之绝对值皆不大于端值之绝对值,则唤之曰“威胁”。既易次数,彼此相异之威胁区间最少可得几何?(这个翻译有点生硬)

窃以为是术 AC 焉,然自题解所记,未之有也。故列码于此,不理解也。是题鄙人之友奆 Luke_Li 为之,何不 % 为?

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const int N = 500005;
#define lowbit(x) ((x)&-x)
int n, a[N], s[N], sgn[N], b[N], cnt[N][2];
queue<int> q;
set<int> st;
void add(int x, int y) {
    while (x <= n) {
        s[x] += y;
        x += lowbit(x);
    }
}
int query(int x) {
    int res = 0;
    while (x) {
        res += s[x];
        x -= lowbit(x);
    }
    return res;
}
int main() {
    read(n);
    rep(i, 1, n) {
        read(a[i]);
        a[i] = abs(a[i]);
        b[i] = a[i];
    }
    sort(b + 1, b + n + 1);
    int tot = unique(b + 1, b + n + 1) - b - 1;
    rep(i, 1, n) a[i] = lower_bound(b + 1, b + n + 1, a[i]) - b;
    rep(i, 1, n) {
        sgn[i] = ((query(a[i]) - query(a[i] - 1)) % 2);
        if (b[a[i]] == 0) sgn[i] = 0;
        while (!st.empty() && (*st.begin()) < a[i]) {
            add(*st.begin(), -query(*st.begin()));
            st.erase(st.begin());
        }
        add(a[i], 1);
        st.insert(a[i]);
    }
    rep(i, 1, n) add(i, -query(i));
    ll ans = 0;
    rep(i, 1, n) {
        ans += cnt[a[i]][sgn[i]];
        while (!st.empty() && (*st.begin()) < a[i]) {
            cnt[*st.begin()][0] = cnt[*st.begin()][1] = 0;
            st.erase(st.begin());
        }
        cnt[a[i]][sgn[i]]++;
        st.insert(a[i]);
    }
    printf("%lld\n", ans);
    return 0;
}

1.6 堆

堆是一棵树,其每个节点都有一个键值,且每个节点的键值都大于等于/小于等于其父亲的键值。

std::priority_queue 是一种大根堆

堆支持的操作有:插入数据、查询最小/最大值(堆顶)、删除最小/最大值(堆顶)等等等等。

例题P2168 [NOI2015] 荷马史诗

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define pll pair<long long, long long>
#define lll __int128
template<typename T> void chkmx(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmn(T &x, const T &y) {x = min(x, y);}
const int N = 100005;
int n, k;
ll w[N];
priority_queue<pll> q;
int num[N * 5];
int main() {
    read(n, k);
    rep(i, 1, n) read(w[i]);
    rep(i, 1, n) q.push(make_pair(-w[i], 0));
    while (k != 2 && q.size() % (k - 1) != 1) {
        q.push(make_pair(0, 0));
    }
    ll ans = 0, mx = 0;
    
    while (q.size() > 1) {
        ll tmp = 0, r = 0;
        rep(i, 1, k) {
            cout << q.top().first << ' ' << q.top().second << '\n';
            tmp += -q.top().first;
            ans += -q.top().first;
            r = max(r, -q.top().second);
            q.pop();
        }
        cout << "-----\n";
        q.push(make_pair(-tmp, -r - 1));
        mx = max(mx, r);
    }
    printf("%lld\n%lld\n", ans, mx + 1);
    return 0;
}

1.7 分块

分块的思想是把原数据划分为若干个块,在每个块上维护一定的信息,使得可以通过处理块而不是原始数据来获得更优秀的时间复杂度。

例题P13978 数列分块入门 3

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define pll pair<long long, long long>
#define lll __int128
template<typename T> void chkmax(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmin(T &x, const T &y) {x = min(x, y);}
const int N = 400005;
ll n, a[N], b[N], block;
ll val[N];
int main() {
    memset(a, 0xc0, sizeof(a));
    read(n);
    rep(i, 1, n) read(a[i]);
    block = sqrt(n);
    for (int i = 1; i <= n; i += block) {
        rep(j, i, i + block - 1) b[j] = a[j];
        sort(b + i, b + i + block);
    }
    rep(i, 1, n) {
        ll op, l, r, c;
        read(op, l, r, c);
        if (op == 0) {
            int L = (l - 1) / block + 1, R = (r - 1) / block + 1;
            if (L == R) {
                rep(i, l, r) a[i] += c;
                rep(i, (L - 1) * block + 1, L * block) b[i] = a[i];
                sort(b + (L - 1) * block + 1, b + L * block + 1);
            } else {
                rep(i, l, L * block) a[i] += c;
                rep(i, (L - 1) * block + 1, L * block) b[i] = a[i];
                sort(b + (L - 1) * block + 1, b + L * block + 1);
                rep(i, (R - 1) * block + 1, r) a[i] += c;
                rep(i, (R - 1) * block + 1, R * block) b[i] = a[i];
                sort(b + (R - 1) * block + 1, b + R * block + 1);
                rep(i, L + 1, R - 1) val[i] += c;
            }
        } else {
            ll L = (l - 1) / block + 1, R = (r - 1) / block + 1, ans = 0xc0c0c0c0c0c0c0c0;
            if (L == R) {
                rep(i, l, r) if (a[i] + val[L] < c) chkmax(ans, a[i] + val[L]);
            } else {
                rep(i, l, L * block) if (a[i] + val[L] < c) chkmax(ans, a[i] + val[L]);
                rep(i, (R - 1) * block + 1, r) if (a[i] + val[R] < c) chkmax(ans, a[i] + val[R]);
                rep(i, L + 1, R - 1) {
                    int res = lower_bound(b + (i - 1) * block + 1, b + i * block + 1, c - val[i]) - b;
                    if (res != (i - 1) * block + 1 && b[res - 1] + val[i] < c) chkmax(ans, b[res - 1] + val[i]);
                }
            }
            printf("%lld\n", (ans == 0xc0c0c0c0c0c0c0c0) ? -1 : ans);
        }
    }
    return 0;
}

2.1 背包 DP

背包 DP 是一种用来解决背包问题的 DP。背包问题中的每个物品通常拥有价值和重量两种因素,要求在总重量不超过上限的情况下获得最高的价值。在下面的问题中,设有 \(n\) 个物品,背包容量(总重量的最大值)为 \(m\)

最常见的背包问题是 0-1 背包问题,要求每种物品只能选一次。考虑设 \(f_{i,j}\) 表示使用前 \(i\) 个物品时,重量不超过 \(j\) 的最大价值。假设 \(f_{i-1,?}\) 已经处理完毕,现在考虑如何使用第 \(i\) 个物品。设第 \(i\) 个物品的重量为 \(w_i\),价值为 \(v_i\),则:

  • 不使用第 \(i\) 个物品,\(f_{i,j}=f_{i-1,j}\)
  • 使用第 \(i\) 个物品,\(f_{i,j}=f_{i-1,j-w_i}+v_i\)

两者取较大值即可。为了降低空间复杂度,可以使用滚动数组,或者直接去掉第 \(1\) 维状态。应当注意状态枚举的顺序,防止错误地修改不应修改的数据。这种做法的时间复杂度显然为 \(O(nm)\)

还有一种背包问题也比较常见,叫完全背包,即每种物品可以使用无限次。参考 0-1 背包的状态定义和转移,有 \(f_{i,j}=\max(f_{i-1,j},f_{i,j-w_i}+v_i)\)。同样地,应当注意这里状态转移的顺序,防止因转移顺序错误导致的问题。这种做法的时间复杂度显然也为 \(O(nm)\)

接下来的一种背包是多重背包,在这个问题中第 \(i\) 种物品最多选 \(k_i\) 次。一种朴素的思想是把 \(k_i\) 次转化为 \(k_i\) 个相同的物品,然后使用 0-1 背包处理,但这样做时间复杂度过高。一种常见的优化思路是二进制分组优化,这是因为任何一个数字都可以通过若干个 \(2\) 的某次幂组合出来,因此考虑把选取次数进行二进制拆分,用 \(2^0+2^1+2^2+\cdots=k_i\) 表示。特殊地,如果 \(k_i+1\) 不是 \(2\) 的整数次幂,则考虑在最后添加非 \(2\) 的整数次幂进行补足。这样操作的时间复杂度为 \(O(m\sum (\log k_i))\)

例题P1987 摇钱树

2.3 数位 DP

数位 DP 是一种用于统计 \([1,r]\) 的正整数中,满足某些神秘性质的数的数量的 DP。一般来说,如果题目给出了一种神秘的条件,求 \([l,r]\) 内满足这种条件的整数数量,而且 \(l,r\) 的数据范围是像 \(10^18\) 这样很大的数,则可以考虑使用数位 DP,分别求 \([1,l-1]\)\([1,r]\) 的答案,然后两个相减。

P13085 [SCOI2009] windy 数(加强版) 是一道比较经典的数位 DP 题目,定义 windy 数为不含前导零且相邻两个数字之差至少为 \(2\) 的正整数,求 \([l,r]\) 中 windy 数的个数。为了处理上下界,一般会在正式开始数位 DP 前把界限按位展开,例如:

long long pd(long long x) {
    int tot = 0;
    do num[++tot] = x % 10; while (x /= 10);
    return dfs(tot, -inf, 1, 1);
}
int main() {
    ...
    cout << pd(r) - pd(l - 1) << '\n';
    return 0;
}

接下来看到 DP 函数 long long dfs(int pos, int lst, bool lim, bool zero)。可以发现数位 DP 的“形参”数相对较多,且一般使用记忆化搜索形式。这四个形参中,有三个对数位 DP 是通用的(有时候可以简化),这三个分别是 poslimzeropos 用于记录当前正在枚举数字的第几位,一般当 pos 达到 \(0\) 时结束递归并返回;lim 用于记录当前的状态是否紧贴上界,例如当当前计算的是 pd(114514) 时,形如 \(1145**\)(星号表示还没有枚举到)的数字就是紧贴上界的;zero 用于记录当前已经枚举到的数字是否全为 \(0\),用于排除一些非法情况。

在本题中,lst 用于记录已经枚举的上一位是什么,这是为了使下一步要枚举的这一位满足 windy 数的形成条件。因此,在 dfs 函数中写下:

long long dfs(int pos, int lst, bool lim, bool zero) {
    if (!pos) return 1; // 位数到了
    if (!lim && !zero && ~dp[pos][lst]) return dp[pos][lst]; // 如果这个状态计算过就直接返回
    long long res = 0; int up = lim ? num[pos] : 9; // up 表示当前枚举数字的上限,这一位不能超过 up
    for (int i = 0; i <= up; i++) { // 枚举当前这一位
        if (abs(i - lst) >= 2) { // 相邻两位之差 ≥ 2
            res += dfs(pos - 1, (zero && i == 0) ? -inf : i, lim && i == up, zero && i == 0); // 递归计算答案
        }
    }
    if (!lim && !zero) dp[pos][lst] = res; // 将已计算的答案保存
    return res;
}

为什么当 limzero 之一为 true 时不记录呢?这是因为:

  • limtrue 的情况只有一种,即全程贴着上界枚举,这个状态只会被访问一次,防止浪费内存;
  • 同样,zerotrue 的情况也只有一种,即每一位都是 \(0\),也只会被访问一次。

因此干脆就不记录这种情况了。

例题P1831 [ICPC 2010 Chengdu R] 杠杆数

杠杆数是指:若指定某一位数为“支点”,则这个数左侧各位与其到“支点”距离的乘积之和等于右侧的。例如,\(4139\) 中指定 \(3\) 为“支点”,则有 \(4\times 2+1\times 1=9\times 1\)。给定 \(l,r\),求 \([l,r]\) 内的杠杆数个数。

这道题直接 DP 不太方便,因此可以考虑在 pd 函数中枚举支点位置,然后对每个位置分别进行一次数位 DP。

考虑设 ll dfs(int pos, bool lim, int cur, int sum)ll 表示 long long)表示当前枚举到第 pos 位,不是/是贴着上界,支点为 cur,目前左侧各乘积之和减右侧为 sum。不设置 zero 是因为这里将全 \(0\) 的情况挪到 pd 函数里处理。

这个函数的形式直接套用上面 windy 数的就可以,留给读者思考。想不通的看下面代码。

现在处理全 \(0\) 串的计数。容易证明,对于非 \(0\) 的杠杆数,它的支点有且仅有 \(1\) 个;对于 \(=0\) 的杠杆数,它的支点个数为这个数的长度,因为左侧和右侧的和一定都是 \(0\)。因此,在 pd 函数里计算答案时,需要减去位数,也就是上面代码中的 tot,以防止 \(0\) 的干扰。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define lll __int128
template<typename T> void chkmax(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmin(T &x, const T &y) {x = min(x, y);}
const int N = 20, M = 10000;
ll l, r, dp[N][N][M];
int num[N];
ll dfs(int pos, bool lim, int cur, int sum) {
    if (!pos) return sum == 0;
    if (sum < 0) return 0; // 如果 sum<0 必然无解,因此不用继续计算,也不用存答案
    if (!lim && ~dp[pos][cur][sum]) return dp[pos][cur][sum];
    ll ans = 0; int up = lim ? num[pos] : 9;
    rep(i, 0, up) ans += dfs(pos - 1, lim && i == up, cur, sum + ((pos > cur) ? ((pos - cur) * i) : ((pos < cur) ? (-(cur - pos) * i) : 0)));
    if (!lim) dp[pos][cur][sum] = ans;
    return ans;
}
ll pd(ll x) {
    memset(dp, -1, sizeof(dp));
    int tot = 0;
    do num[++tot] = x % 10; while (x /= 10);
    ll ans = 0;
    rep(i, 1, tot) ans += dfs(tot, 1, i, 0);
    return ans - tot;
}
int main() {
    read(l, r);
    printf("%lld\n", pd(r) - pd(l - 1));
    return 0;
}

2.4 树形 DP

树形 DP 是一种在树上的 DP,一般形式是令 \(f_{u, ?}\) 表示以 \(u\) 为根的子树有 \(?\) 的某种状态时的答案。

2.4.1 树上背包

树上背包是一种转移状态的方法,核心思想是枚举 \(u\) 的儿子 \(v\),对 \(v\) 进行计算,然后把 \(v\) 的答案合并到 \(u\) 已求完的部分。通用形式是:\(f_{u,i+j}\leftarrow f_{u,i}\oplus f_{v,j}\)

例题P1411 树

给一棵无根树,求删去任意条边后所有连通块大小乘积的最大值。

先随便指定一个点作为树根,这里用 \(1\) 号点。设 \(f_{i,j}\) 为以 \(i\) 为根的子树内,\(i\) 所在连通块有 \(j\) 个点时,\(i\) 所在连通块外所有连通块乘积的最大值。于是有 \(f_{u,i}=\max\limits_{v\in son(u)}(f_{u,i}\times j\times f_{v,j})\)\(f_{u,i+j}=\max\limits_{v\in son(u)}(f_{u,i}\times f_{v,j})\),答案为 \(\max(i\times f_{1,i})\)。注意这道题:1. 需要高精度运算;2. 卡空间、卡时间。可以考虑使用 memsetmemcpy 等方法,并考虑使用 C++20 O2 以加速运行(能降低一点常数)。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(short i=(s);i<=(t);i++)
#define per(i, s, t) for(short i=(s);i>=(t);i--)
const int N = 710;
const int LEN = 120;
short zero[LEN];
inline void clear(short a[]) {
    memcpy(a, zero, sizeof(zero));
}
inline void input(short a[], int b) {
    clear(a);
    short i = 0;
    while (b) {
        a[i] = b % 10;
        b /= 10;
        ++i;
    }
}
inline void print(short a[]) {
    int i;
    for (i = LEN - 1; i >= 1; --i)
        if (a[i] != 0)
            break;
    for (; i >= 0; --i)
        putchar(a[i] + '0');
    putchar('\n');
}
inline void mul(short a[], short b[], short c[]) {
    clear(c);
    for (int i = 0; i < LEN - 1; ++i) {
        for (int j = 0; j <= i; ++j)
            c[i] += a[j] * b[i - j];

        if (c[i] >= 10) {
            c[i + 1] += c[i] / 10;
            c[i] %= 10;
        }
    }
}
inline bool isless(short a[], short b[]) {
    for (int i = LEN - 1; i >= 0; i--) if (a[i] != b[i]) return a[i] < b[i];
    return false;
}
static short n;
vector<short> g[N];
short f[N][N][LEN], siz[N], tmp[N][LEN], res[LEN], maxn[LEN], tmp1[LEN], tmp2[LEN];
inline void dfs(const int &u, const int &fa) {
    siz[u] = 1;
    input(f[u][1], 1);
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u); 
        rep(i, 0, N - 1) clear(tmp[i]);
        clear(maxn);
        rep(i, 1, siz[v]) {
            input(tmp1, i);
            mul(f[v][i], tmp1, tmp2);
            if (isless(maxn, tmp2)) memcpy(maxn, tmp2, sizeof(tmp2));
        }
        rep(i, 1, siz[u]) {
            rep(j, 1, siz[v]) {
                mul(f[u][i], f[v][j], res);
                if (isless(tmp[i + j], res)) memcpy(tmp[i + j], res, sizeof(res));
            }
            mul(f[u][i], maxn, res);
            if (isless(tmp[i], res)) memcpy(tmp[i], res, sizeof(res));
        }
        siz[u] += siz[v];
        rep(i, 1, siz[u]) {
            memcpy(f[u][i], tmp[i], sizeof(tmp[i]));
        }
    } 
}
int main() {
    read(n);
    rep(i, 1, n - 1) {
        short u, v;
        read(u, v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    clear(tmp2);
    rep(i, 1, n) {
        input(tmp1, i);
        mul(f[1][i], tmp1, res);
        if (isless(tmp2, res)) memcpy(tmp2, res, sizeof(res));
    }
    print(tmp2);
    return 0;
}

2.4.2 换根 DP

换根 DP 是一种树形 DP 的技巧,核心思想是在以某个点为根进行一次树形 DP 后,通过“换根”操作得到已其他点为根时的答案。

例题P2986 [USACO10MAR] Great Cow Gathering G

每个点有点权 \(c_i\),定义“不方便度”为各点的点权和与该点到“集合点”路径长的乘积之和,要最小化这个值。

不讲了。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define gc getchar
template<typename T> void read(T &x) {
    char c = gc(); int f = 1; x = 0;
    while (!isdigit(c)) f = (c == '-' ? -f : f), c = gc();
    while (isdigit(c)) x = (x << 3) + (x << 1) + c - '0', c = gc();
    x *= f;
}
template<typename T, typename ... Args> void read(T &x, Args& ... args) {read(x); read(args...);}
#define rep(i, s, t) for(int i=(s);i<=(t);i++)
#define per(i, s, t) for(int i=(s);i>=(t);i--)
#define ll long long
#define ull unsigned long long
#define pii pair<int,int>
#define pll pair<long long, long long>
#define lll __int128
template<typename T> void chkmax(T &x, const T &y) {x = max(x, y);}
template<typename T> void chkmin(T &x, const T &y) {x = min(x, y);}
const int N = 1e5 + 5;
ll n, c[N], siz[N], sum;
vector<pii> g[N];
ll ans[N];
void dfs(int u, int fa) {
    ans[u] = 0; siz[u] = c[u];
    for (auto [v, w] : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
        ans[u] += ans[v] + w * siz[v];
        siz[u] += siz[v];
    }
}
void pd(int u, int fa, int w) {
    if (u != 1) ans[u] = ans[fa] + (sum - siz[u] - siz[u]) * w;
    for (auto [v, w] : g[u]) {
        if (v == fa) continue;
        pd(v, u, w);
    }
}
int main() {
    read(n);
    rep(i, 1, n) read(c[i]), sum += c[i];
    rep(i, 1, n - 1) {
        int u, v, w;
        read(u, v, w);
        g[u].push_back(make_pair(v, w));
        g[v].push_back(make_pair(u, w));
    }
    dfs(1, 0);
    pd(1, 0, 0);
    ll finalans = 0x7f7f7f7f7f7f7f7f;
    rep(i, 1, n) {
        cout << ans[i] << ' '; 
        chkmin(finalans, ans[i]);
    }cout << '\n';
    printf("%lld\n", finalans);
    return 0;
}

2.5 其他 DP 优化

2.5.1 单调栈、单调队列优化 DP

2.5.2 斜率优化 DP

2.5.3 四边形不等式优化 DP(决策单调性优化 DP)

2.6 DP 套 DP

点击查看代码

posted @ 2026-08-13 22:07  cwkapn  阅读(6)  评论(0)    收藏  举报