DP专题

分治优化DP

分治优化1D/1D dp

对于一类

\[f(x) = \min_{k = y}^{x - 1} w(l, r) \]

即所有 \(w(l,r)\) 事先已知,且 \(f(x)\) 满足决策单调性(即 \(w(l, r)\) 满足区间包含单调性和四边形不等式),而且 \(w(l, r)\) 不便于直接计算时,可采用分治法计算:

以下 \(w(l, r) = g(l) + w'(l + 1, r)\)\(w'(l, r)\)calc(l, r)计算,在整个分治过程中左右端点扫过的长度是 \(\mathcal{O}(n \log n)\) 的。

void dc(int l, int r, int d, int kl, int kr) {
    int mid = (l + r) >> 1;
    int k = mid - 1;
    f[mid] = g[k] + calc(k + 1, mid);
    rep(t,kl,min(kr,mid - 2)) {
        long long w = calc(t + 1, mid);
        if(f[mid] > g[t] + w)
            k = t, f[mid] = g[t] + w;
    }
    if(l < mid) dc(l, mid - 1, d, kl, k);
    if(r > mid) dc(mid + 1, r, d, k, kr);
}

题目

CF868F Yet Another Minimization Problem
题面

给定 \(n, k\) 和一个序列 \(a_{1 \dots n}\) ,其中 \(1 \leq a_i \leq n\) 。将序列划分为 \(k\) 段,有一个评估值为每段相同颜色对数目之和。求最小化这个值。

题解

\(w(l, r)\) 为区间 \([l,r]\) 的相同颜色对数目之和。显然向两边单点插入的复杂度是 \(\mathcal{O}(\log n)\) 的,如果预处理了颜色的桶那么单点插入是 \(\mathcal{O}(1)\) 的。

暴力计算一个区间或者暴力合并两个区间的复杂度都是区间长度的,只能拆下来一个个插入。

\(f(x, t)\)\(a_{1 \dots x}\) 划分成 \(t\) 段的最小评估值,显然有:

\[f(x, t) = \min_{1 \leq k \leq x - 1} f(k, t - 1) + w(k + 1, x) \\ f(x, 0) = 0 \\ \]

状态数为 \(\mathcal{O}(nk)\) ,转移复杂度为 \(\mathcal{O}(n)\)

决策单调性证明:

\(w(l, r)\) 显然满足区间包含单调性。更长的区间相同数目的颜色对显然不会少于更短的区间。

四边形不等式:

\(cnt(l, r, a_i)\)\([l, r]\)\(a_i\) 相同的数的个数。

\[\begin{aligned} w(l - 1, r + 1) + w(l, r) &= w(l - 1, r) + cnt(l - 1, r, a_{r + 1}) + w(l, r + 1) - cnt(l, r, a_{r + 1}) \\ &= w(l - 1, r) + w(l, r + 1) + (cnt(l - 1, r, a_{r + 1}) - cnt(l, r, a_{r + 1})) \\ &= w(l - 1, r) + w(l, r + 1) + [a_{l - 1} = a_{r + 1}] \\ &\geq w(l - 1, r) + w(l, r + 1) \end{aligned} \]

\(f(x, t - 1)\) 在计算 \(f(x, t)\) 时已知,因而可以用分治优化递推,复杂度 \(\mathcal{O}(nk \log n)\)

int a[maxn], buc[maxn];
long long f[maxd][maxn];

int pl = 1, pr = 0, w;
inline void add(int idx) { w += buc[a[idx]]; ++ buc[a[idx]]; }
inline void del(int idx) { -- buc[a[idx]]; w -= buc[a[idx]]; }
inline int calc(int l, int r) {
    while(pr < r) add(++ pr);
    while(pr > r) del(pr --);
    while(pl < l) del(pl ++);
    while(pl > l) add(-- pl);
    return w;
}

void dc(int l, int r, int d, int kl, int kr) {
    int mid = (l + r) >> 1;
    int k = mid - 1;
    f[d][mid] = f[d - 1][mid - 1];
    rep(t,kl,min(kr,mid - 2)) {
        long long w = calc(t + 1, mid);
        if(f[d][mid] > f[d - 1][t] + w)
            k = t, f[d][mid] = f[d - 1][t] + w;
    }
    if(l < mid) dc(l, mid - 1, d, kl, k);
    if(r > mid) dc(mid + 1, r, d, k, kr);
}

int main() {
    int n, k;
    scanf("%d %d", &n, &k);
    rep(i,1,n) scanf("%d", a + i);
    rep(i,1,n) f[1][i] = f[1][i - 1] + buc[a[i]], ++ buc[a[i]];
    rep(i,1,n) buc[i] = 0;
    rep(i,2,k) dc(1, n, i, 1, n);
    printf("%lld\n", f[k][n]);
    return 0;
}

cdq分治维护凸包优化dp

cdq分治在递归过程中枚举了元素之间两两的所有贡献。

但有时候要求按照一定的顺序处理。

cdq分治是在一个类似于线段树的结构上搜索并枚举贡献。

对于有搜索顺序限制要求的场景,需要对搜索顺序进行调整。例如,在维护斜率优化dp时,一个点只有在处理完时(被前面所有点贡献到),才能贡献后续点,因此要用中序遍历。

对于需要特殊处理的枚举方式,常用cdq分治,因为对于半边的元素可以自由决定插入的方式,例如在维护凸包优化dp时,可以对半边在插入前按照 \(x\) 排序从而简单构建凸包,避免写平衡树。

void work(int l, int r) {
    if(l == r) return ;
    int mid = (l + r) >> 1;
    work(l, mid);
    hull.clear();
    double mx = 0;
    vector<pt> tmp(mid - l + 1);
    rep(i,l,mid) mx = max(f[i], mx), tmp[i - l] = calc(i);
    sort(tmp.begin(), tmp.end(), [&](pt &x, pt &y) { return x.x < y.x; });
    for(auto &it : tmp) hull.insert(it);
    rep(i,mid + 1, r) f[i] = max({mx, f[i], getf(hull.qry(-a[i] / b[i]), a[i], b[i])});
    work(mid + 1, r);
}

斜率优化dp

主要是推柿子题。
例如有dp方程

\[f(i) = \min_{1 \leqslant j \leqslant i} \{ f(j) + p(j)q(i) + r(i) \} \]

的形式。
在柿子中斤出现两个关于 \(j\) 的函数且关于变量 \(j\) 线性组合, 那么可以用斜率优化来解。
考虑去掉 \(\min\) 并变形柿子 :

\[f(j) = -q(i)p(j) + r(i) + f(i) \]

对于当前的\(i\), 不妨将之前的每个 \(j\) 看作一个点 \((p(j), f(j))\) , 则当前柿子变成了一条直线。斜率 \(k = -q(i)\) , 截距 \(b = r(i) + f(i)\)。且要过某一点 \((p(j), f(j))\) 其中 \(-q(i)\)\(r(i)\) 是固定的, 因而我们要最小化 \(f(i)\) 实际上就是最小化截距 \(b\), 而取到最小截距的点对应的 \(j\) 就是最优决策点。
有一个结论, 除了在下凸壳上的点, 其余点都不会成为最优决策点。直观上很容易接受。
下凸壳按照 \(x\) 递增的顺序排列的点之间的线段的斜率是单调增的, 直观上我们发现对于某一当前的斜率 \(-q(i)\) 而言, 最优决策点一定在某一个 \(j\) 取到, 这个点与上一个点形成的线段的斜率 \(\leqslant\) \(-q(i)\) 而与后一个点线段的斜率 \(>-q(i)\)
在一般情况下, 我们在下凸包上二分斜率就可以得到最优决策点。如果 \(-q(i)\) 的值关于 \(i\) 单调递增则可以用指针单调扫过去在均摊 \(\mathcal{O(1)}\) 得到最优决策点。
如果 \(p(j)\) 是单调给出的, 那么我们可以直接将新来的点尝试插入到下凸壳尾部。如果不是, 则需要用平衡树来维护下凸壳中的点, 在插入前找到该下标所在的位置, 若该下标在下凸壳下方, 则需要插入这个点, 且需要向两边尝试删点。
在维护下凸壳的时候要注意特殊处理横坐标相等的情况(斜率会变成 \(\infty\))。平衡树维护凸壳的时候要注意插到两边的情况。在凸壳上二分的时候要注意最后一个点不能算斜率了。
\(\max\) 改为在上凸壳上做。

acwing 301任务安排2

题解

运用 贡献提前计算 的思想易得dp方程

\[f(i) = \min \{ f(j) + s(\operatorname{sum} c_n - \operatorname{sum} c_j) + \operatorname{sum}t_i (\operatorname{sum}c_i - \operatorname{sum} c_j) \} \]

去掉 \(\min\) 并做变形得

\[f(j) = f(i) + (s + \operatorname{sum}t_i) \operatorname{sum} c_j - \operatorname{sum}c_i \operatorname{sum}t_i - s \operatorname{sum} c_n\\ \]

由于 \(c_i, t_i > 0\) , 故 \(\operatorname{sum}c_j\)\(s + \operatorname{sum} t_i\) 均单调递增。直接维护凸壳并用单指针扫一遍即可。

code
struct point {
    ll x, y;
};
point operator - (const point& p1, const point& p2) { return {p1.x - p2.x, p1.y - p2.y}; }
const int maxn = 3e5+5;
int c[maxn], t[maxn], n, s;
ll f[maxn];

vector<point> v;
const double cmp = 1e-7;
inline int sign(double x) { return x > cmp ? 1 : (x < cmp ? -1 : 0); }
inline int cross(point a, point b) { return sign(1.0 * a.x * b.y - 1.0 * a.y * b.x); }

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    scanf("%d %d", &n, &s);
    rep(i,1,n) scanf("%d %d", t + i, c + i);
    rep(i,1,n) t[i] += t[i-1], c[i] += c[i-1];
    f[1] = t[1] * c[1] + s * c[n];
    v.pb({0, 0}); v.pb({c[1], f[1]});
    int head = 0;
    rep(i,2,n) {
        while(head + 1 < v.size() && (v[head+1].y - v[head].y) < (s + t[i]) * (v[head+1].x - v[head].x)) ++head;
        f[i] = v[head].y - 1ll * (s + t[i]) * v[head].x + 1ll * t[i] * c[i] + 1ll * s * c[n];
        point cur = {c[i], f[i]};
        while(head + 1 < v.size() && cross(v[v.size()-1]-v[v.size()-2], cur-v[v.size()-1]) <= 0) v.pop_back();
        v.pb(cur);
    }
    printf("%lld\n", f[n]);
    return 0;
}

acwing 303 任务安排3

题解

在上题的基础上取消了 \(t_i > 0\) 的条件。这意味着斜率不单调给出, 因此需要在下凸壳上二分斜率, 多了一个 \(\log\)

code
typedef long long ll;
typedef double db;

const int maxn = 3e5+5;
int c[maxn], t[maxn], v[maxn], n, tot;
db f[maxn];

inline db calc(int x) { return x == tot ? 1e200 : (f[v[x+1]] - f[v[x]]) / (c[v[x+1]] - c[v[x]]); }

int find(int k) {
    int l = 0, r = tot;
    while(l < r) {
        int mid = (l + r) >> 1;
        if(calc(mid) <= k) l = mid + 1;
        else r = mid;
    }
    return v[l];
}

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    int s;
    scanf("%d %d", &n, &s);
    rep(i,1,n) scanf("%d %d", t + i, c + i), c[i] += c[i-1], t[i] += t[i-1];
    v[0] = 0;
    rep(i,1,n) {
        int j = find(s + t[i]);
        f[i] = f[j] + 1.0 * s * (c[n] - c[j]) + 1.0 * t[i] * (c[i] - c[j]);
        while(tot && 1.0 * (f[i]-f[v[tot]]) * (c[v[tot]] - c[v[tot-1]]) <= 1.0 * (f[v[tot]] - f[v[tot-1]]) * (c[i] - c[v[tot]])) --tot;
        v[++tot] = i;
    }
    printf("%.0f\n", f[n]);
    return 0;
}

CF311B Cat Transport

在序列上 dp 时考虑将每次决策通过改写状态变成连续的一段 , 便于 dp 的转移。

[NOI2007] 货币兑换

推完式子之后大力用平衡树维护凸包优化 dp 转移。

李超线段树相关的dp优化

[CEOI2017]Building Bridges

考虑暴力dp, 设\(f_i\)为打通到第\(i\)号节点的最小代价, 有

\[\displaystyle f_i = \min\limits_{1 \leqslant j < i}\{f_j + \sum_{k=j+1}^i w_k + (h_i-h_j)^2\} \]

\(S_i = \sum_{i=1}^n w_i\), 变形式子

\[f_i = h_i^2+S_{i-1}+\min\limits_{1 \leqslant j < i}\{-2h_jh_i+f_j+h_j^2-S_j\} \]

每个\(h_j\)都会对后面的所有\(h_i\)进行更新, 这是一个关于\(h_i\)的一次函数, \(k=-2h_j, \space b = f_j + h_j^2-S_j\)
用李超线段树即可维护即可在\(\mathcal{O(\log n)}\)求出单点的函数最值。
复杂度\(\mathcal{O(n \log n)}\)

#include <bits/stdc++.h>
#define endl '\n'
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define per(i,a,b) for(int i=a;i>=(b);--i)
#define repp(i,a,b) for(int i=a;i<(b);++i)
#define perr(i,a,b) for(int i=a;i>(b);--i)

using namespace std;

typedef long long ll;

const int inf = 0x3f3f3f3f, maxn = 123456, maxh = 1234567;
const ll linf = 0x3f3f3f3f3f3f3f3f;

ll h[maxn], k[maxn], b[maxn], s[maxn], f[maxn];

int dat[maxh << 2];

inline ll calc(int id, int x) {
	return k[id] * x + b[id];
}

void insert(int p, int lp, int rp, int id) {
	if(!dat[p]) { dat[p] = id; return; }
	if(lp == rp) {
		if(calc(dat[p], lp) > calc(id, lp)) dat[p] = lp;
		return;
	}
	int mid = lp + rp >> 1;
	ll pre = calc(dat[p], mid), cur = calc(id, mid);
	if(k[dat[p]] < k[id]) {
		if(cur < pre) insert(p<<1|1, mid+1, rp, dat[p]), dat[p] = id;
		else insert(p<<1, lp, mid, id);
	} else if(k[dat[p]] > k[id]) {
		if(cur < pre) insert(p<<1, lp, mid, dat[p]), dat[p] = id;
		else insert(p<<1|1, mid+1, rp, id);
	} else {
		if(cur < pre) dat[p] = id;
	}
}

ll qry(int p, int lp, int rp, int x) {
	if(lp == rp) return calc(dat[p], x);
	int mid = lp + rp >> 1;
	if(x <= mid) return min(calc(dat[p], x), qry(p<<1, lp, mid, x));
	else return min(calc(dat[p], x), qry(p<<1|1, mid+1, rp, x));
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n;
	ll mh = 0;
	cin >> n;
	rep(i, 1, n) cin >> h[i], mh = max(h[i], mh);
	rep(i, 1, n) cin >> s[i];
	rep(i, 2, n) s[i] += s[i-1];
	k[0] = b[0] = inf;
	f[1] = 0;
	k[1] = -2 * h[1], b[1] = -s[1] + h[1]*h[1];
	insert(1, 0, mh, 1);
	rep(i, 2, n) {
		f[i] = h[i] * h[i] + s[i-1] + qry(1, 0, mh, h[i]);
		k[i] = -2 * h[i], b[i] = f[i] + -s[i] + h[i] * h[i];
		insert(1, 0, mh, i);
	}
	cout << f[n] << endl;
	return 0;
}
``


#### [NC17140] Longest Path(2018牛客多校1 - H)

##### 题解
(注意,这里只是展示一下用法,这个题比较简洁的做法应该是斜率优化)

点分治统计过根的路径,用后面的子树去查询前面的子树。然后倒着做一遍得到反向的结果。
中间要维护的距离中比较难处理的是连接根与子树的两条产生的贡献。设正在查询的子树的那条权值为 $x$ , 另外的子树那边的那条为 $y$ 。这个贡献就是 $(x-y)^2 = x^2 - 2xy + y^2$ 。  
看起来像是个 $2$ 次函数 , $y$ 是与 $x$ 无关的常量。 但是考虑道其实当前子树查询的每一段都包含 $x^2$ , 我们的数据结构其实起的作用只是比较大小并挑出最大的那个 , 所以可以把 $x^2$ 拆出来 , 这样只要维护一次函数即可 , 直接上李超线段树。  
$$
f(u) = \max_{v}\{(x_u-x_v)^2\} = x_u^2 + \max_{v}\{-2x_ux_v + x_v^2\}
$$
维护的其实是后半部分。

trick : 前往后统计但是要双向的时候,把边reverse倒着做一遍。
property : 实际上一个子树的最优一定是该子树内的最好的情况 , 只需要把这个值作为一个子树对后面查询的贡献丢到李超树里面就行了 , 实际上到这一步就应该想到普通树形dp一下就能实现了qwq。

##### code
```cpp
/*
  sol.cpp -- NC17140 Longest Path - divide and conquer on tree + LiChao segment tree
*/

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define per(i,a,b) for(int i=(a);i>=(b);--i)
#define repp(i,a,b) for(int i=(a);i<(b);++i)
#define perr(i,a,b) for(int i=(a);i>(b);--i)
#define pb push_back
#define rz resize

using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef double db;
typedef long double ldb;
typedef vector<int> VI;
typedef vector<ll> VL;
typedef vector<ldb> VD;

template<class T> inline void read(T &x) {
    x = 0; char ch;
    do{ch=getchar();} while(ch<'0'||ch>'9');
    do{x=(x<<3)+(x<<1)+(x&15),ch=getchar();} while(ch>='0'&&ch<='9');
}
int Mod = 1;
inline int Inc(int x, int y) { return (x += y) < Mod ? x : x - Mod; }
inline int Dec(int x, int y) { return (x -= y) < 0 ? x + Mod : x; }
inline int Mul(int x, int y) { return 1ll * x * y % Mod; }
inline int Power(int x, int y) {
    int ret = 1%Mod;
    for(; y; y>>=1) {
        if(y&1) ret=(ll)ret*x%Mod;
        x=(ll)x*x%Mod;
    }
    return ret;
}

template<class T> inline T abs(T x) { return x > 0 ? x : -x; }
template<class T> inline void cmin(T &x, T &y) { x = x > y ? y : x; }
template<class T> inline void cmax(T &x, T &y) { x = x > y ? x : y; }
// template<class T> inline T min(T x, T y) { return x < y ? x : y; }
// template<class T> inline T max(T x, T y) { return x > y ? x : y; }

template<class T>
class BIT { public:
    const int maxN;
    vector<T> c;
    BIT(int size) : maxN(size) { c.rz(size + 1); }
    void add(int x, T v) { for(; x <= maxN; x += x&(-x)) c[x] += v; }
    T qry(int x) { T ret = 0; for(; x; x -= x&(-x)) ret += c[x]; return ret; }
};

const int N = 100010;
const ll linf = 99999999999999999;

int abl[N], maxc;

struct edge {
    int x, w;
    edge(int x, int w) : x(x), w(w) {}
};
vector< vector<edge> > G;

ll k[N], b[N], ltt = 0;
ll f[N];

ll calc(int x, int id) {
    return 1ll * k[id] * x + b[id];
}

int dat[N<<2], col[N<<2], nc = 0;
inline void insert(int p, int lp, int rp, int id) {
    if(col[p] < nc) {
        dat[p] = id; col[p] = nc;
        return;
    }
    int mid = lp + rp >> 1;
    ll mval = calc(mid, id), nval = calc(mid, dat[p]);
    if(lp == rp) {
        if(calc(lp, id) > calc(lp, dat[p])) dat[p] = id;
        return;
    }
    if(k[id] < k[dat[p]]) {
        if(mval < nval) insert(p<<1, lp, mid, id);
        else insert(p<<1|1, mid + 1, rp, dat[p]), dat[p] = id;
    } else if(k[id] > k[dat[p]]) {
        if(mval < nval) insert(p<<1|1, mid+1, rp, id);
        else insert(p<<1, lp, mid, dat[p]), dat[p] = id;
    } else if(mval > nval) {
        dat[p] = id;
    }
}

ll qry(int p, int lp, int rp, int x) {
    if(col[p] < nc) return -linf;
    int mid = lp + rp >> 1;
    if(x <= mid) return max(calc(x, dat[p]), qry(p<<1, lp, mid, x));
    else return max(calc(x, dat[p]), qry(p<<1|1, mid + 1, rp, x));
}

int stk[N], siz[N], top, fa[N];
void dfs1(int cur, int pre) {
    stk[++top] = cur; siz[cur] = 1;
    fa[cur] = pre;
    for(auto e : G[cur]) {
        if(abl[e.x] && e.x != pre) {
            dfs1(e.x, cur);
            siz[cur] += siz[e.x];
        }
    }
}

int findroot(int cur) {
    top = 0;
    dfs1(cur, 0);
    if(top == 1) return cur;
    rep(i,1,top) {
        int xx = stk[i], hf = top >> 1;
        if(top - siz[xx] <= hf) {
            bool flg = true;
            for(edge e : G[xx]) {
                if(abl[e.x] && siz[e.x] > hf && e.x != fa[xx]) {
                    flg = false; break;
                }
            }
            if(flg){
                return xx;
            }
        }
    }
    return 0;
}

ll dis[N];
void dfs(int cur, int pre, int prew, ll distance) {
    stk[++top] = cur; dis[top] = distance;
    for(edge e : G[cur])
        if(abl[e.x] && e.x != pre) {
            dfs(e.x, cur, e.w, distance + 1ll * (e.w - prew) * (e.w - prew));
        }
}

void work(int cur) {
    cur = findroot(cur);
    if(top == 1) return;
    ltt = 0; ++nc;
    for(auto e : G[cur])
        if(abl[e.x]) {
            top = 0;
            dfs(e.x, cur, e.w, 0);
            ll dis1 = 1ll * e.w * e.w + qry(1, 1, maxc, e.w), md = 1;
            rep(i,1,top) {
                if(dis[md] < dis[i]) md = i;
                f[stk[i]] = max(f[stk[i]], max(dis1 + dis[i], dis[i]));
            }
            f[cur] = max(f[cur], dis[md]);
            k[++ltt] = -2 * e.w; b[ltt] = 1ll * e.w * e.w + dis[md];
            insert(1, 1, maxc, ltt);
        }
    abl[cur] = 0;
    for(auto e : G[cur])
        if(abl[e.x])
            work(e.x);
}

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    int n, a, b, c;
    while(~scanf("%d", &n)) {
        G.clear();
        G.resize(n + 1);
        maxc = 0;
        memset(f, 0, sizeof(f));
        repp(i,1,n) {
            scanf("%d %d %d", &a, &b, &c);
            G[a].push_back(edge(b, c));
            G[b].push_back(edge(a, c));
            maxc = max(c, maxc);
        }
        memset(abl, 1, sizeof(abl));
        work(1);
        memset(abl, 1, sizeof(abl));
        rep(i,1,n) reverse(G[i].begin(), G[i].end());
        work(1);
        rep(i,1,n) printf("%lld\n", f[i]);
    }
    return 0;
}

实际上树形dp统计叶子们到这个点的距离即可(有点类似换根地)。维护一次函数最值也可以用斜率优化来做。

数位dp

acwing1081 度的数量

题意

求给定区间 \([X,Y]\) 中满足下列条件的整数个数:这个数恰好等于 \(K\) 个互不相等的 \(B\) 的整数次幂之和。
数据范围 :
$ 1 \leqslant X \leqslant Y \leqslant 2^{31}−1,
1 \leqslant K \leqslant 20, 2 \leqslant B \leqslant 10 $

题解

\(k\)\(B\) 的不同正整数次幂和看成 \(B\) 进制下所有数位只有 \(0\)\(1\)\(1\) 的个数之和为 \(k\) 的数。然后做数位dp。

code
const int maxn = 100010, maxd = 35;
int binom[maxd][maxd];

void get_binom(int n) {
    rep(i,0,n) binom[i][0] = 1;
    rep(i,1,n) rep(j,1,i) binom[i][j] = binom[i-1][j] + binom[i-1][j-1];
}

int k, b;
int dp(int x) {
    if(x == 0) return 0;
    vector<int> nums; while(x) nums.pb(x % b), x /= b;
    int lst = 0, ans = 0;
    per(i,nums.size()-1,0) {
        if(nums[i]) {
            ans += binom[i][k-lst];
            if(nums[i] > 1) {
                if(lst < k) ans += binom[i][k-lst-1];
                break;
            } else {
                ++lst;
                if(lst > k) break;
            }
        }
        if(i == 0 && lst == k) ++ans;
    }
    return ans;
}

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    int x, y;
    scanf("%d %d %d %d", &x, &y, &k, &b);
    get_binom(maxd-1);
    printf("%d\n", dp(y) - dp(x - 1));
    return 0;
}

acwing1802 数字游戏

题意

指定一个整数闭区间 \([a,b]\) , 求这个区间内满足数位自左至右单调不减的数的个数。
数据范围 : \(1 \leqslant a \leqslant b \leqslant 2^{31}−1\)

题解

首先需要预处理以某个数为开头的位数为 \(n\) 的不降数的个数 \(f_{n,i} = \sum_{x = i}^9 f_{n-1, x}\)
然后再做数位 dp 。

code
const int maxd = 15, maxn = 35;
int binom[maxd][maxd], f[maxn][10];
void get_binom() {
    repp(i,0,maxd) binom[i][0] = 1;
    repp(i,1,maxd) rep(j,1,i) binom[i][j] = binom[i-1][j] + binom[i-1][j-1];
    repp(i,1,maxd) rep(j,1,i) binom[i][j] += binom[i][j-1];
}
void get_fs() {
    rep(i,0,9) f[1][i] = 1;
    rep(i,2,maxn-1) rep(j,0,9) rep(k,j,9) f[i][j] += f[i-1][k];
}

int dp(int n) {
    if(n < 10) return n + 1;
    vector<int> nums; while(n) nums.pb(n % 10), n /= 10;
    int ans = 0;
    repp(i,0,nums[nums.size()-1]) ans += f[nums.size()][i];
    per(i,nums.size()-2,0) {
        if(nums[i] >= nums[i+1]) {
            rep(x,nums[i+1],nums[i]-1) ans += f[i+1][x];
        } else if(nums[i] < nums[i+1]) break;
        if(i == 0) ++ans;
    }
    return ans;
}

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    get_binom(); get_fs();
    int a, b;
    while(~scanf("%d %d", &a, &b)) {
        printf("%d\n", dp(b) - dp(a-1));
    }
    return 0;
}

[SCOI2009] acwing1803 Windy数

题意

给定区间 \([A,B]\) , 求在此区间内不含前导零且相邻两个数字之差 \(\geqslant 1\) 的正整数的个数。
数据范围 : \(1 \leqslant A \leqslant B \leqslant 2 \times 10^9\)

题解

首先用dp求出某个数字 \(x\) 开头的数位长度为 \(n\) 的数字的个数 \(f_{n,x}\) :

f_{n,x} = \sum_{\left| x - k \right| > 1} f_{n-1,k} \\
f_{1,x} = 1 \quad x \in [0,9]

然后数位 dp 来求解。
做第一位的左枝统计的时候应当注意对含前导 \(0\) 的数的处理。

code
const int maxd = 9, maxn = 10;
int f[maxn+10][maxd+10];

void get_fs() {
    rep(i,0,maxd) f[1][i] = 1;
    rep(i,2,maxn) rep(j,0,maxd) rep(k,0,maxd) if(k - j > 1 || k - j < -1) f[i][j] += f[i-1][k];
}

int dp(int n) {
    if(n < 10) return n + 1;
    vector<int> nums; while(n) { nums.pb(n % 10); n /= 10; }
    int ans = 1;
    repp(x,1,nums[nums.size()-1]) ans += f[nums.size()][x];
    repp(len,1,nums.size()) rep(i,1,maxd) ans += f[len][i];
    per(i,nums.size()-2,0) {
        rep(x,0,nums[i]-1) if(abs(nums[i+1]-x) > 1) ans += f[i+1][x];
        if(abs(nums[i] - nums[i+1]) <= 1) break;
        if(i == 0) ++ans;
    }
    return ans;
}

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    get_fs();
    int a, b;
    scanf("%d %d", &a, &b);
    printf("%d\n", dp(b) - dp(a-1));
    return 0;
}

acwing1085 / HDU2089 不要62

题意

给定区间 \([n, m]\) , 求在此区间内满足数位中不包含 \(4\) 且不包含连续的 \(62\) 的数的个数。
数据范围 : \(1 \leqslant n \leqslant m \leqslant 10^9\)

code
const int maxn = 10, maxd = 9;
int f[maxn][maxd+5];
void init() {
    rep(i,0,maxd) f[1][i] = (i==4) ? 0 : 1;
    rep(i,2,maxn-1) rep(j,0,9) rep(k,0,9) if(j != 4 && k != 4 && (j != 6 || k != 2)) f[i][j] += f[i-1][k];
}

int dp(int n) {
    if(n < 10) return n < 4 ? n + 1 : n;
    vector<int> nums; while(n) nums.pb(n % 10), n /= 10;
    int ans = 0;
    per(i,nums.size()-1,0) {
        repp(x,0,nums[i]) if(x != 4 && (i == (signed)nums.size()-1 || x != 2 || nums[i+1] != 6)) ans += f[i+1][x];
        if(nums[i] == 4 || (nums[i] == 2 && i != (signed)nums.size() - 1 && nums[i+1] == 6)) break;
        if(i == 0) ++ans;
    }
    return ans;
}

int main() {
    if(fopen("yl.in", "r")) {
        freopen("yl.in", "r", stdin);
        freopen("yl.out", "w", stdout);
    }
    init();
    int a, b;
    while(~scanf("%d %d", &a, &b) && a && b) {
        printf("%d\n", dp(b) - dp(a-1));
    }
    return 0;
}

DDP(这部分是抄来的)

SP1043 GSS1 - Can you answer these queries I

题意简述:给出长度为 \(n\) 的序列 \(a\) 以及 \(m\) 组询问,对每组询问 \([l_i,r_i]\) 回答区间内的最大子段和。

数据范围:\(n,m\le 5\times 10^4\)

这题有一个经典做法,就是用线段树维护区间的最大前缀和、最大后缀和、总和、最大子段和再进行合并,但这并不是我们今天要讨论的。

考虑朴素的 dp 转移方程,设 \(f_i\) 表示以 \(i\) 结尾的最大子段和,\(g_i\) 表示 \(\max\limits_{j\le i} f_j\),有转移

\[f_i=\max(f_{i-1}+a_i,a_i) \\ g_i=\max(g_{i-1},f_i) \]

我们需要构造一个转移矩阵,使得 \(\begin{bmatrix} f_{i-1} & g_{i-1} & \cdots\end{bmatrix}\) \(\begin{bmatrix} a_i & \cdots \\ \vdots & \ddots \end{bmatrix}\) \(=\) \(\begin{bmatrix} f_i & g_i & \cdots\end{bmatrix}\)

此时你会发现,我们需要用到一个矩阵乘法里不涉及的运算:\(\max\)

于是我们需要重定义矩阵乘法:\(C_{i,j}=\max \limits_{k}A_{i,k}+B_{k,j}\)

这里也可以使用 \(\min\),具体按照 dp 转移方程而定。

回到本题,我们可以将状态设为:\(\begin{bmatrix} f_j & 0 & g_j \end{bmatrix}\),并构造出这样的转移矩阵:\(\begin{bmatrix}a_i & -\infty & a_i \\ a_i & 0 & a_i \\ -\infty & -\infty & 0\end{bmatrix}\)

手动模拟一下转移过程,可以发现得到的新矩阵为:\(\begin{bmatrix}\max\{f_j+a_i,0+a_i,g_j+-\infty\} & \max\{f_j+-\infty,0+0,g_j+-\infty\} & \max\{f_j+a_i,0+a_i,g_j+0\}\end{bmatrix}\),整理得: \(\begin{bmatrix}\max\{f_j+a_i,a_i\} & 0 & \max\{f_j+a_i,a_i,g_j\}\end{bmatrix}\)

对照我们之前推出来的式子,我们惊喜的发现这个矩阵就是 \(\begin{bmatrix}f_i & 0 & g_i\end{bmatrix}\)

不难发现,我们新定义的运算是满足结合律的(证明请自行百度),于是可以利用线段树快速求出一段区间的矩阵的乘积,从而进行转移。

再来分析一下时间复杂度,一次矩阵乘法的复杂度是 \(O(a^3)\) 的,其中 \(a\) 为矩阵的边长,线段树查询一段区间的信息是 \(O(\log n)\) 的,故总时间复杂度为 \(O((n+m)a^3\log n)\)

SP1716 GSS3 - Can you answer these queries III

题意简述:给出长度为 \(n\) 的序列 \(a\) 以及 \(m\) 组操作,操作 0 x y 表示把 \(a_x\) 修改为 \(y\),操作 1 l r 表示询问区间 \([l,r]\) 的最大子段和。

数据范围:\(n,m\le 5\times 10^4\)

可以发现其实就是 GSS1 多了个单点修改,那么只要在线段树上修改对应点的信息即可。

CF750E New Year and Old Subsequence

题意简述:给出长度为 \(n\) 的序列 \(a\) 以及 \(m\) 组询问,对每组询问回答 \(a\) 的子串 \([l_i,r_i]\) 至少删去多少个字符后才能使得其不包含子序列 2016 而包含 2017。

数据范围:\(n,m\le 2\times 10^5\)

\(f_{i,0/1/2/3/4}\) 分别表示当前子串末尾为 \(i\) 时包含$ \varnothing/2/20/201/2017$ 最少需要删去多少个字符,那么有

\[ f_{i,0}=f_{i-1,0}+[s_i=2] \\ f_{i,1}=\min(f_{i-1,1}+[s_i=0],f_{i-1,0}[s_i=2]) \\ f_{i,2}=\min(f_{i-1,2}+[s_i=1],f_{i-1,1}[s_i=0]) \\ f_{i,3}=\min(f_{i-1,3}+[s_i=7\ \lor\ s_i=6],f_{i-1,2}[s_i=1]) \\ f_{i,4}=\min(f_{i-1,4}+[s_i=6],f_{i-1,3}[s_i=7]) \]

构造出转移矩阵后进行动态 dp 即可。

一个 trick:

可以发现我们的答案矩阵其实只有一行,那么在线段树上查询的时候只用完成第一行的矩阵乘法。(注意是查询,在线段树 push_up 的时候还是得用正常的矩阵乘法)。

使用该 trick 后 966ms -> 607ms。优化还是较明显的。

CF573D Bear and Cavalry

题意简述:有 \(n\) 个人和 \(n\) 匹马,第 \(i\) 个人对应第 \(i\) 匹马,要求每个人都不能骑自己对应的马。第 \(i\) 个人的能力值为 \(w_i\),第 \(i\) 匹马的能力值为 \(h_i\),第 \(i\) 个人骑第 \(j\) 匹马的能力值为 \(w_ih_j\),整个军队的总能力值为 \(\sum w_ih_j\)(一个人只能骑一匹马,一匹马只能被一个人骑)。有 \(m\) 个操作,每次给出 \(a,b\),交换 \(a\)\(b\) 对应的马。每次操作后你都需要输出最大的总能力值。

数据范围:\(n\le 3\times10^4\)\(m\le 10^4\)

有这样的结论:我们把 \(w,h\) 排序后,每个人 \(i\) 匹配的马必定在 \([i-2, i+2]\) 中。

这里直接引用 @_sys 的证明:

于是设 \(f_i\)表示前 \(i\) 个人和前 \(i\) 匹马匹配完成的最大权值,则有转移 \(f_i=\max\limits_{j\ge i-3}{f_j+calc(j+1,i)}\)

其中 \(calc(l,r)\) 表示 \([l,r]\) 区间内满足每个人不骑自己对应的马的最大总能力值,可以 \(O((r-l+1)!)\) 枚举每种情况计算。

最后构造出转移矩阵并进行动态 dp 即可。

P4719 【模板】"动态 DP"&动态树分治

题意简述:给定一棵 \(n\) 个点的树,\(i\) 号点的点权为 \(a_i\)。有 \(m\) 次操作,每次操作给定 \(x,y\),表示修改点 \(x\) 的权值为 \(y\)。你需要在每次操作之后求出这棵树的最大权独立集的权值大小。

数据范围:\(n,m\le 10^5\)

先考虑没有修改的情况下怎么做。首先先选取 1 号点作为全树的根。然后我们设 \(f_{i,0}\) 表示不选择 \(i\) 号点时,以 \(i\) 号点为根的子树的最大权独立集;\(f_{i,1}\) 表示选择 \(i\) 号点时,以 \(i\) 号点为根的子树的最大权独立集。则有如下转移方程:

\[ f_{i,0}=\sum\limits_{j\in son_i} \max(f_{j,0},f_{j,1}) \\ f_{i,1}=\sum\limits_{j\in son_i} f_{j,0} + a_i \]

答案就是 \(\max(f_{1, 0}, f_{1, 1})\)

然后你熟练地准备构造转移矩阵,却发现无从下手:序列上的 dp 是 “一对一” 转移,而树上的 dp 是 “多对一” 转移,即要考虑到每个儿子。

那么如何把这个转化成 “一对一” 转移的形式呢?这就要用到树链剖分了。

\(g_{i,0}\) 表示 \(i\) 号点的所有轻儿子可取可不取,且不取 \(i\) 的最大权独立集,\(g_{i,1}\) 表示 \(i\) 号点的所有轻儿子都不取,且取了 \(i\) 的最大权独立集,\(j\)\(i\) 的重儿子。则有如下转移方程:

\[f_{i,0}=g_{i,0}+\max(f_{j,0},f_{j,1}) \\ f_{i,1}=g_{i,1}+ f_{j,0} \]

这样就在转移时就只需要考虑重儿子了。相应的转移矩阵也不难构造,请自行尝试。

那么问题来了:带修改操作,如何维护 \(g\) 呢?

对于一条重链,求出这条重链上矩阵的乘积,这就是这条重链链头对它父亲的贡献(显然重链链头是他父亲的轻儿子),将父亲的 \(g\) 减去贡献后,更新重链上需要修改的点的矩阵,再将新的乘积对父亲的 \(g\) 的贡献求出并加上。完成这一切后,父亲成为新的需要修改的点,父亲所在重链成为新的需要处理贡献的重链,就这样处理下去,直到重链链头为根为止。

由于树剖带一个 \(\log\),因此总时间复杂度为 \(O(na^3 \log^2 n)\)

[NOIP2018 提高组] 保卫王国

题意简述:给定一棵 \(n\) 个点的树,给 \(i\) 号点染色的花费为 \(a_i\),要求任2个相邻点至少有1个被染色。给出 \(m\) 组询问,每次强制两个点的状态(染/不染),求出每次询问的最小花费。

数据范围:\(n,m\le 10^5\)

\(f_{i,0}\) 表示 \(i\) 号点不染色的最小花费,\(f_{i,1}\) 表示 \(i\) 号点染色的最小花费,则有如下转移:

\[ f_{i,0}=\sum\limits_{j\in son_i} f_{j,1} \\ f_{i,1}=\sum\limits_{j\in son_i} \min(f_{j,0},f_{j,1})+a_i \]

于是按照与模板题相同的方法动态 dp 即可。

posted @ 2022-10-04 20:33  zhaozixu2006  阅读(30)  评论(0编辑  收藏  举报