动态规划类型选讲

数据结构优化DP​

引言

在状态转移过程中, 我们通常需要在某个区间范围内进行择优, 选出最佳决策点.

而数据结构通常可以维护出转移的最优决策.

数据结构优化DPDP的实质为优化"转移".

例题

UVA12983 The Battle of Chibi

题目大意:

计算在长度为nn的序列中, 严格单调递增且长度为mm的子序列的数量.

答案对1e9+71e9+7取模, 多组数据.

1mn103,1ai1091\leq m\leq n\leq10^3,1\leq a_i\leq 10^9.

题解:

f[i][j]f[i][j]表示由区间[1,j][1,j]中的数构成的以a[j]a[j]结尾的子序列中,长度为ii的严格递增子序列的数量.

f[i][j]=k<j,a[k]<a[j]f[i1][k]\large f[i][j]=\displaystyle \sum_{k<j,a[k]<a[j]} f[i-1][k]

特殊的, i=1i=1时,则有f[i][j]=1f[i][j]=1.

时间复杂度:O(n2m)O(n^2m).

观察所需要维护的信息:

(1)(1): 原序列中的位置k<jk<j.

(2)(2): a[k]<a[j]a[k]<a[j].

(3)(3): f[i1][k]\sum f[i-1][k].

解决方法:

(1)(1): 枚举顺序使得$k<j $.

(2)+(3)(2)+(3): 发现维护的是满足a[k]<a[j]a[k]<a[j]f[i1][k]f[i-1][k]的总和.

利用数据结构进行优化:

a[j]a[j]作为下标,f[i1][j]f[i-1][j]作为权值建立树状数组.

对于每次进行的f[i][j]f[i][j]的决策, 将a[j]a[j]作为数据结构所维护的关键字,

每次计算f[i][j]f[i][j]时查询前面已加入的且关键字<a[j]<a[j]f[i1][k]f[i-1][k]的总和.

最后将f[i1][j]f[i-1][j]加入树状数组.

时间复杂度:O(nmlogn)O(nmlogn).

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e3 + 5, mod = 1e9 + 7;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int T, n, m, a[N], b[N], tr[N], f[N][N];
void change(int x, int v) {
	for( ; x <= n + 1; x += x & (-x)) (tr[x] += v) %= mod;
}
int query(int x) {
	int res = 0;
	for( ; x; x -= x & (-x)) (res += tr[x]) %= mod;
	return res;
}
int main() {
	T = read();
	for(int t = 1; t <= T; ++ t) {
		n = read(); m = read();
		memset(f, 0, sizeof(f));
		for(int i = 1; i <= n; ++ i) {
			a[i] = b[i] = read();
			f[1][i] = 1;	//特殊处理长度为 1 的情况;
		}
		sort(b + 1, b + n + 1);	//离散化;
		LL cnt = unique(b + 1, b + n + 1) - b - 1;
		for(int i = 1; i <= n; ++ i) {
			a[i] = lower_bound(b + 1, b + cnt + 1, a[i]) - b;
		}
		for(int i = 2; i <= m; ++ i) {	//枚举长度;
			memset(tr, 0, sizeof(tr));	//清空树状数组;
			for(int j = 1; j <= n; ++ j) {	//枚举子序列结尾的位置(同时保证了 k < j);
				f[i][j] = query(a[j] - 1);	//树状数组维护满足a[k]<a[j]的 ∑f[i-1][k];
				change(a[j], f[i-1][j]);	//维护树状数组的信息;
			}
		}
		int ans = 0;
		for(int i = 1; i <= n; ++ i) {	//统计以位置 i 为结尾长度为 m 的答案贡献;
			(ans += f[m][i]) %= mod;
		}
		printf("Case #%d: %d\n", t, ans);
	}
	return 0;
}

P3287 [SCOI2014]方伯伯的玉米田

题目大意:

给定一个长度为nn的序列, 对序列进行kk次操作.

每次操作可以选取区间[l,r][l,r]使得此区间内每个元素h[i]+1h[i]+1,

kk次操作后所形成的新序列中最长不下降子序列长度的最大值.

1<n<104,1<k500,1hi50001<n<10^4,1<k\leq500,1\leq h_i\leq5000.

题解:

结论: 每一次操作的区间右端点一定为nn.

证明: 反证法.

假设进行的某次操作为区间[l,r][l,r]rnr\neq n.

则将此区间更改为[l,n][l,n],则相对于原序列[r+1,n][r+1,n]会被+1+1.

所形成的新序列中最长不下降子序列长度一定不会变小.

证毕.

f[i][j]f[i][j]表示区间[1,i][1,i]中, hih_i进行了jj次操作后最长不下降子序列长度的最大值.

f[i][j]=Max(f[k][l]+1)(1k<i,0lj,h[i]+jh[k]+l)\large f[i][j]=Max(f[k][l]+1)(1\leq k<i,0\leq l\leq j,h[i]+j\geq h[k]+l)

时间复杂度:O(n2k2)O(n^2*k^2)

观察所需要维护的东西为二维前缀最大值.

候选决策集合只扩大,因此可以利用二维树状数组进行维护.

然而, 发现限制条件中有h[i]+jh[k]+l\large h[i]+j\geq h[k]+l, 难以用树状数组满足限制.

考虑将高度的限制加入状态.

f[j][k]f[j][k]表示区间[1,i][1,i](从左到右枚举ii)中, 以h=jh= j(这里的hh为操作后的hh)被操作kk次的位置结尾的答案.

可以用tmptmp变量代替f[ ][ ]f[\ ][\ ]数组.

利用类似背包的思想滚动掉ii这一维.

时间复杂度:O(nlognklogk)O(nlogn*klogk).

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e4 + 5;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int n, k, ans, maxl, h[N], tr[N][6505];
void change(int x, int y, int val){
    for(int i = x; i <= k; i += i & (-i)) {
        for(int j = y; j <= maxl; j += j & (-j)) {
        	tr[i][j] = Max(tr[i][j], val);
        }
    }
}
int query(int x, int y) {
	int res = 0;
	for(int i = x; i > 0; i -= i & (-i)) {
		for(int j = y; j > 0; j -= j & (-j)) {
			res = Max(res, tr[i][j]);
		}
	}
	return res;
}
int main() {
	freopen("a.in", "r", stdin);
	freopen("a.out", "w", stdout);
	n = read(); k = read();
	k ++;	//树状数组不能处理下标为 0 的情况, 整体位移;
	for(int i = 1; i <= n; ++ i) {
		h[i] = read();
		maxl = Max(maxl, h[i]);
	}
	maxl += k;
	for(int i = 1; i <= n; ++ i) {	//枚举处理到的右边界;
		for(int j = k; j; -- j) {	//枚举操作次数;
            int tmp = query(j, h[i] + j) + 1;
			ans = Max(ans, tmp);
            change(j, h[i] + j, tmp);
		}
	}
	printf("%d\n", ans);
	return 0;
}

P2605 [ZJOI2010]基站选址

题目大意:

nn个村庄位于一条直线, 每个村庄有一个范围sis_i.

要求每个村庄的范围内有至少11个村庄被建立基站, 否则需要赔偿wiw_i.

建立基站需要花费cic_i,求最小总花费.

1n20000,1k1001\leq n\leq 20000,1\leq k\leq100.

题解:

f[i][j]f[i][j]表示考虑前ii个村庄, 第jj个修建基站的村庄为ii的最小花费.

f[i][j]=Min(f[k][j1]+cost[k][i])+w[i](j1k<i)\large f[i][j]=Min(f[k][j-1]+cost[k][i])+w[i](j-1\leq k<i)

cost[i][j]cost[i][j]表示村庄iijj建立基站但iijj之间的村庄未建立基站所需的赔偿费用.

cost[i][j]cost[i][j]预处理O(n2)O(n^2)

时间复杂度:O(n2k)O(n^2k)

优化①:

发现对于f[ ][ ]f[\ ][\ ]数组来说, 第二维为xx时, 只有第二维为x1x-1的数据是有用的.

j<ij<i, 因此可以压缩第二维.

在最外层枚举jj.

f[i]=Min(f[k]+cost[k][i])+w[i](j1k<i)\large f[i]=Min(f[k]+cost[k][i])+w[i](j-1\leq k<i)

优化②:

优化计算cost[i][j]cost[i][j].

对于任意村庄ii, 记录记录它所能被覆盖的左右边界st[i], ed[i]st[i],\ ed[i],

即最左端, 最右端可以覆盖到ii的基站位置, 利用二分查找处理.

用邻接表记录eded值为ii的村庄有哪些, 在这些村庄之后建立基站就覆盖不到ii了.

这样当我们推导i+1i+1时, 若从村庄11~st[k]1(ed[k]=i)st[k]-1(ed[k]=i)转移过来则必定要赔偿村庄kk的费用,

我们可以考虑用线段树维护f[k]+cost[k][i]f[k]+cost[k][i]的值.

即在区间[1,st[k]1][1,st[k]-1]加上村庄kk的费用, 而转移即在区间[1,i1][1,i-1]f[k]+cost[k][i]f[k]+cost[k][i]的最小值.

时间复杂度:O(nlongnk)O(nlongn*k).

考虑在末尾增加一个建设费用为00的基站, 因为在推导ii时, 是默认在ii处建立基站的.

而且对前面没有影响.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e4 + 5;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
vector <int> v[N];
int n, k, ql, qr, d[N], c[N], s[N], w[N], l[N], r[N], f[N];
struct Segment_tree{	//标记永久化线段树;
    int minl[N << 2], tag[N << 2];
    void pushup(int x) { minl[x] = Min(minl[x<<1], minl[x<<1|1]); }
    void build(int x,int l,int r) {
        tag[x] = 0;
        if(l == r) return (void)minl[x] = f[l];
        int mid = (l + r) >> 1;
        build(x<<1, l, mid);
        build(x<<1|1, mid + 1, r);
        pushup(x);
    }
    void maintain(int x, int l, int r) {
        if(l != r) pushup(x);
        minl[x] += tag[x];
    }
    void change(int x, int l, int r, int v) {
        if(ql <= l && qr >= r) tag[x] += v;
        else { 
            int mid = (l + r) >> 1;
            if(ql <= mid) change(x<<1, l, mid, v);
            if(qr > mid) change(x<<1|1, mid + 1, r, v);
        } 
        maintain(x, l, r);
    }
    int query(int x, int l, int r, int v) {
        if(ql <= l && qr >= r) return minl[x] + v;
        int res = 2e9, mid = (l + r) >> 1;
        if(ql <= mid) res = Min(res,query(x<<1, l, mid, v + tag[x]));
        if(qr > mid) res = Min(res,query(x<<1|1,mid + 1, r , v + tag[x]));
        return res;
    }
} tr;
int main() {
	n = read(); k = read();
	for(int i = 2; i <= n; ++ i) d[i] = read();
	for(int i = 1; i <= n; ++ i) c[i] = read();
	for(int i = 1; i <= n; ++ i) s[i] = read();
	for(int i = 1; i <= n; ++ i) w[i] = read();
	n ++; k ++;
	d[n] = 2e9;
	for(int i = 1; i <= n; ++ i) {
		l[i] = lower_bound(d + 1, d + n + 1, d[i] - s[i]) - d;
		r[i] = lower_bound(d + 1, d + n + 1, d[i] + s[i]) - d;
		if(d[i] + s[i] < d[r[i]]) r[i] --;
		v[r[i]].push_back(i);
	}
	int ans = 2e9;
	for(int j = 1; j <= k; ++ j) {	//最外层枚举修建的基站个数;
		if(j == 1) {
			int tot = 0;
			for(int i = 1; i <= n; ++ i) {	//枚举修建位置;
				f[i] = tot + c[i];	//赔偿费用 + 建造费用;
				for(int tmp = 0; tmp < v[i].size(); ++ tmp) {	//统计赔偿费用;
					tot += w[v[i][tmp]];	//在往后的推导中就要赔偿这些村庄的费用;
				}
			}
			ans = Min(ans, f[n]);
			continue;
		}
		tr.build(1, 1, n);	//线段树维护 f[k]+cost[k][i]的最小值;
		for(int i = 1; i <= n; ++ i) {	//枚举修建位置;
			ql = 1, qr = i-1;
			int add = qr ? tr.query(1, 1, n, 0) : 0;	//i != 1 / i = 1(特判, 优化复杂度);
			f[i] = add + c[i];
			for(int tmp = 0; tmp < v[i].size(); ++ tmp) {
				ql = 1, qr = l[v[i][tmp]] - 1;
				if(qr >= 1) tr.change(1, 1, n, w[v[i][tmp]]);	//保证合法的情况下增添赔偿费用;
			}
		}
		ans = Min(ans, f[n]);
	}
	printf("%d\n", ans);
	return 0;
}

斜率优化DP

决策单调性

对于DPDP方程

f[i]=Min(a[i]b[j]+c[j]+d[i])f[i]=Min(a[i]*b[j]+c[j]+d[i])

其中a[i], b[i], c[i], d[i]a[i],\ b[i],\ c[i],\ d[i]为关于$\ i\ $的函数, 且 bb 函数单调递增. ----------①

利用数学归纳法证明ff函数具有决策单调性.

1.1.归纳假设:

假设 ii 有两个决策点j, k(j<k)j,\ k(j<k), 且 kk 的决策要比 jj 好, 即满足:

a[i]b[j]+c[j]+d[i]a[i]b[k]+c[k]+d[i], j<ka[i]*b[j]+c[j]+d[i] \geq a[i]*b[k]+c[k]+d[i],\ j<k----------②

2.2.归纳推理:

此时 ii 后面的状态 i+1i+1, 为简单起见, 设a[i+1]=a[i]v, v>0a[i+1]=a[i]-v,\ v > 0, 即aa函数单调递减.

证明决策单调性, 即证:

a[i+1]b[j]+c[j]+d[i+1]a[i+1]b[k]+c[k]+d[i+1]a[i+1]*b[j]+c[j]+d[i+1] \geq a[i+1]*b[k]+c[k]+d[i+1]

代入a[i+1]=a[i]va[i+1]=a[i]-v

(a[i]v)b[j]+c[j]+d[i+1](a[i]v)b[k]+c[k]+d[i+1](a[i]-v)*b[j]+c[j]+d[i+1] \geq (a[i]-v)*b[k]+c[k]+d[i+1]

即证:

a[i]b[j]+vb[k]+c[j]a[i]b[k]+vb[j]+c[k]a[i]*b[j]+v*b[k]+c[j] \geq a[i]*b[k]+v*b[j]+c[k]

由②得:

a[i]b[j]+c[j]a[i]b[k]+c[k]a[i]*b[j]+c[j] \geq a[i]*b[k]+c[k]----------③

由①得:

b[k]>b[j]b[k]>b[j]

v>0\because v>0

vb[k]vb[j]\therefore v*b[k] \geq v*b[j]----------④.

+③+④得:

a[i]b[j]+vb[k]a[i]b[k]+vb[j]+c[k]a[i]*b[j]+v*b[k] \geq a[i]*b[k]+v*b[j]+c[k]

证毕.

我们将由决策单调性得出的式子展开:

a[i]b[j]+c[j]+d[i]a[i]b[k]+c[k]+d[i], j<ka[i]*b[j]+c[j]+d[i] \geq a[i]*b[k]+c[k]+d[i],\ j<k

将式子变形得:

a[i](b[k]b[j])c[k]c[j]-a[i]*(b[k]-b[j]) \geq c[k]-c[j]

b[k]b[j]>0\because b[k]-b[j]>0

a[i]c[k]c[j]b[k]b[j]\therefore -a[i] \geq \displaystyle \frac{c[k]-c[j]}{b[k]-b[j]}

记斜率

slope(k, j)=c[k]c[j]b[k]b[j]slope(k,\ j)=\displaystyle \frac{c[k]-c[j]}{b[k]-b[j]}

从单调队列的角度分析:

1.a[i]slope(q[head], q[head+1])-a[i] \geq slope(q[head],\ q[head+1]).

q[head]q[head]q[head+1]q[head+1]前加入, 且q[head]q[head]的决策不如q[head+1]q[head+1]优, 则可以将队首poppop.

2.slope(q[tail1], q[tail])>slope(q[tail], i)slope(q[tail-1],\ q[tail]) > slope(q[tail],\ i).

假设在后面存在一个a[t]a[t]使得a[t]slope(q[tail1], q[tail])-a[t] \geq slope(q[tail-1],\ q[tail]).

则在poppopq[tail1]q[tail-1]之后, 由于a[t]slope(q[tail], i)-a[t] \geq slope(q[tail],\ i).

q[tail]q[tail]也一定会被poppop.

q[tail]q[tail]实际上是无用的, 可以直接将其poppop.

时间复杂度:O(n)O(n).

对于DPDP方程:

f[i]=Min(a[i]b[j]+c[j]+d[i])f[i]=Min(a[i]*b[j]+c[j]+d[i])

方程满足: aa函数单调递减, bb函数单调递增.

去掉MinMin函数及移项得:

c[j]+d[i]=a[i]b[j]+f[i]c[j]+d[i]=-a[i]*b[j]+f[i]

x=b[j]x=b[j], y=c[j]+d[i]y=c[j]+d[i]则有:

y=a[i]x+f[i]y=-a[i]*x+f[i]

a[i]-a[i]为直线的斜率, f[i]f[i]为直线的截距, 直线过点P(b[j], c[j]+d[i])P(b[j],\ c[j]+d[i]).(d[i]d[i]相对于当决策ii来说是一个常数).

将每一个可用的决策jj抽象为一个点P(b[j], c[j]+d[i])P(b[j],\ c[j]+d[i]).

线性规划的思想, 将这条斜线自下往上平移所遇到的第一个点, 即为可以使f[i]f[i]取得最小值的决策.

因此我们需要维护一个下凸壳, 将一定不会对答案产生贡献的点删除.

利用单调队列维护这个凸壳, 因为要保证凸壳的下凸性, 所以我们得到poppop队尾的条件:

slope(q[tail1], q[tail])>slope(q[tail], i)slope(q[tail-1],\ q[tail])>slope(q[tail],\ i)

考虑在什么时候poppop队首元素(基于f[i]f[i]取最小值):

1.1.斜率a[i]-a[i]单调递增(函数aa单调递减).

a[i]>slope(q[head], q[head+1])-a[i]>slope(q[head],\ q[head+1]).

2.2.斜率不单调.

此时无法poppop队首, 应使用二分查找或三分查找队列中的最优解.

二分法:

假设要在凸包上二分查找斜率为kk的切线, 取中间的midmid号点.

若存在mid+1mid+1且与midmid点的斜率小于kk, 则l=mid+1l=mid+1;

若存在mid1mid-1且与midmid点的斜率大于kk, 则r=mid1r=mid-1;

否则midmid即为切点.

总结

"形"的角度:方便理解, 对于cdqcdq分治维护凸包可以清晰了解.

"数"的角度:决策单调性的证明, 或DP方程较为复.

在"形"的角度来求解斜率优化并不会用到决策单调性.

若满足决策单调性, 则一定会对aa函数和bb函数的单调性有要求.

例题

P3195 [HNOI2008]玩具装箱TOY

题目大意:

n(n5e4)n(n\leq5e4)个玩具分成若干组, 每组玩具放到一个容器中, 要求每个容器中的玩具编号连续.

将第jj~ii个玩具放入一个容器中, 则容器的长度为x=ij+k=jiCkx=i-j+\displaystyle \sum_{k=j}^iC_k中.

题解:

设前缀和为sum[i]sum[i], f[i]f[i]表示前ii个物品放到11个容器中得最小费用.

考虑将第j+1j+1~ii个玩具放到一个容器中.

f[i]=Min(f[j]+(ij1+sum[i]sum[j]L)2)(j<i)f[i]=Min(f[j]+(i-j-1+sum[i]-sum[j]-L)^2)(j<i)

时间复杂度:O(n2)O(n^2).

下面进行斜率优化(以下称两点ABAB所形成的直线斜率为slope(A,B)slope(A,B)):

为简化运算,我们令a[i]=sum[i]+i, b[j]=sum[j]+j+L+1a[i]=sum[i]+i,\ b[j]=sum[j]+j+L+1.

去掉MinMin并代入a[i]a[i]b[j]b[j]

f[i]=f[j]+(a[i]b[j])2f[i]=f[j]+(a[i]-b[j])^2

展开得

f[i]=f[j]+a[i]22a[i]b[j]+b[j]2f[i]=f[j]+a[i]^2-2*a[i]*b[j]+b[j]^2

移项得

f[j]+b[j]2=2a[i]b[j]+f[i]a[i]2f[j]+b[j]^2=2*a[i]*b[j]+f[i]-a[i]^2

x=b[j],y=f[j]+b[j]2x=b[j],y=f[j]+b[j]^2,则有

y=(2a[i])x+f[i]a[i]2y=(2*a[i])x+f[i]-a[i]^2

x,yx,y只与jj有关, 其他项只与ii有关.

则对于每一个可供选择的jj,我们可以将其抽象为一个点P(b[j],f[j]+b[j]2)P(b[j],f[j]+b[j]^2).

又因为对于当前所求的ii来说,a[i]a[i]是固定的.

所以每个点可以对应一条直线,此直线的截距为f[i]a[i]2f[i]-a[i]^2.

我们需要求f[i]f[i]的最小值,a[i]a[i]对于当前的ii来说又是一个定值.

因求f[i]f[i]的最小值即可以通过求截距的最小值.

直线的斜率为2a[i]=2(sum[i]+i)2*a[i]=2*(sum[i]+i),恒为正值.

因此, 本题中的可能最优点PP组成了一个下凸包.

更具体的,满足条件的最优PjP_j为第一个slope(Pj,Pj+1)>2a[j]slope(P_j,P_{j+1})>2*a[j]的点.

利用单调队列维护凸包.

时间复杂度:O(n)O(n).

slope(q[head],q[head+1])<2a[i]slope(q[head],q[head+1])<2*a[i],则q[head]q[head]一定不是最优解.

slope(i,q[tail1])<slope(q[tail1],q[tail])slope(i,q[tail-1])<slope(q[tail-1],q[tail]), 说明PtailP_{tail}在凸包内部, 一定不是最优解.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5e4 + 5;
typedef long long LL;
typedef double DB;
LL read() {
	LL x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
LL n, L, q[N];
DB sum[N], f[N];
DB a(LL i) { return sum[i] + i; }
DB b(LL i) { return a(i) + L + 1; }
DB X(LL i) { return b(i); }
DB Y(LL i) { return f[i] + b(i) * b(i); }
DB slope(LL i, LL j) { return (Y(i) - Y(j)) / (X(i) - X(j)); }
int main() {
	n = read(); L = read();
	for(int i = 1; i <= n; ++ i) {
		sum[i] = sum[i-1] + read();
	}
	LL head = 1, tail = 1;
	for(int i = 1; i <= n; ++ i) {
		while(head < tail && slope(q[head], q[head+1]) < 2 * a(i)) head ++;
		f[i] = f[q[head]] + (a(i) - b(q[head])) * (a(i) - b(q[head]));
		while(head < tail && slope(i, q[tail-1]) < slope(q[tail-1], q[tail])) tail --;
		q[++ tail] = i;
	}
	printf("%lld\n", (LL)f[n]);
	return 0;
}

P3628 [APIO2010]特别行动队

题目大意:

n(n1e6)n(n\leq1e6)名士兵分组, 要求每组中士兵编号连续.

一组士兵(i(i~j)j)的初始战斗力为k=ijxk\displaystyle \sum_{k=i}^{j}x_k, 修正战斗力为x=ax2+bx+c(a,b,c)x'=ax^2+bx+c(a,b,c为给定常数).

最大化士兵修正战斗力.

题解:

设前缀和为sum[i]sum[i], f[i]f[i]表示前ii名士兵分成若干组的最大修正战斗力.

f[i]=Max(f[j]+a(sum[i]sum[j])2+b(sum[i]sum[j])+c)(j<i)f[i]=Max(f[j]+a*(sum[i]-sum[j])^2+b*(sum[i]-sum[j])+c)(j<i)

去掉MaxMax

f[i]=f[j]+a(sum[i]sum[j])2+b(sum[i]sum[j])+cf[i]=f[j]+a*(sum[i]-sum[j])^2+b*(sum[i]-sum[j])+c

拆开得

f[i]=f[j]+asum[i]22asum[i]sum[j]+asum[j]2+bsum[i]bsum[j]+cf[i]=f[j]+a*sum[i]^2-2*a*sum[i]*sum[j]+a*sum[j]^2+b*sum[i]-b*sum[j]+c

移项及合并同类项得

f[j]+asum[j]2=(2asum[i]+b)sum[j]+f[i]asum[i]2bsum[i]cf[j]+a*sum[j]^2=(2*a*sum[i]+b)*sum[j]+f[i]-a*sum[i]^2-b*sum[i]-c

x=sum[j], y=f[j]+asum[j]2x=sum[j],\ y=f[j]+a*sum[j]^2.

则有

y=(2asum[i]+b)x+f[i]asum[i]2bsum[i]cy=(2*a*sum[i]+b)x+f[i]-a*sum[i]^2-b*sum[i]-c

直线的斜率为2asum[i]2*a*sum[i], 截距为f[i]asum[i]2bsum[i]cf[i]-a*sum[i]^2-b*sum[i]-c.

对于决策jj, 所抽象成的为点P(sum[j], f[j]+asum[j]2)P(sum[j],\ f[j]+a*sum[j]^2).

5a1, sum[i]>0\because-5\leq a \leq -1,\ sum[i]>0.

2asum[i](, 0)\therefore 2*a*sum[i]\in(-\infty,\ 0).

利用单调队列维护一个上凸包即可.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e6 + 5;
typedef long long LL;
typedef double DB;
LL read() {
	LL x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
LL n, a, b, c, q[N];
DB sum[N], f[N];
DB X(LL i) { return sum[i]; }
DB Y(LL i) { return f[i] + a * sum[i] * sum[i]; }
DB slope(LL i, LL j) { return (Y(i) - Y(j)) / (X(i) - X(j)); }
int main() {
	n = read();
	a = read(); b = read(); c = read();
	for(int i = 1; i <= n; ++ i) {
		sum[i] = sum[i - 1] + read();
	}
	LL head = 1, tail = 1;
	for(int i = 1; i <= n; ++ i) {
		while(head < tail && slope(q[head], q[head+1]) > 2 * a * sum[i] + b) head ++;
		int tmp = sum[i] - sum[q[head]];
		f[i] = f[q[head]] + a * tmp * tmp + b * tmp + c;
		while(head < tail && slope(i, q[tail-1]) > slope(q[tail-1], q[tail])) tail --;
		q[++ tail] = i;
	}
	printf("%lld\n", (LL)f[n]);
	return 0;
}

P2120 [ZJOI2007]仓库建设

题目大意:

共有n(n1e6)n(n\leq 1e6)个​工厂, 每个工厂有pip_i件物品.

且工厂ii到工厂11的距离为xi(x1=0)x_i(x_1=0), 在工厂ii建立仓库的费用为cic_i.

要在某些工厂建立仓库, 使总费用(建造费用+运输费用)最小.

每个工厂中的物品只能被运往编号更大的工厂.

一件产品运送11个单位距离的费用是11.

使每个物品最终所在的工厂都建造了仓库.

题解:

就近原则, 一段连续的工厂中的物品一定会放在一个最近的仓库中,

sum1[i]=k=1ip[k]sum1[i]=\displaystyle \sum_{k=1}^ip[k], sum2[i]=k=1ip[i]x[i]sum2[i]=\displaystyle \sum_{k=1}^ip[i]*x[i], f[i]f[i]为将前ii件物品都放如某些仓库的最小费用.

则有

f[i]=Min(f[j]+x[i](sum1[i]sum1[j])(sum2[i]sum2[j]))+c[i]f[i]=Min(f[j]+x[i]*(sum1[i]-sum1[j])-(sum2[i]-sum2[j]))+c[i]

去掉MinMin

f[i]=f[j]+x[i](sum1[i]sum1[j])(sum2[i]sum2[j])+c[i]f[i]=f[j]+x[i]*(sum1[i]-sum1[j])-(sum2[i]-sum2[j])+c[i]

展开得

f[i]=f[j]+x[i]sum1[i]x[i]sum1[j]sum2[i]+sum2[j]+c[i]f[i]=f[j]+x[i]*sum1[i]-x[i]*sum1[j]-sum2[i]+sum2[j]+c[i]

移项及合并同类项得

f[j]+sum2[j]=x[i]sum1[j]+f[j]+sum2[i]x[i]sum1[i]c[i]f[j]+sum2[j]=x[i]*sum1[j]+f[j]+sum2[i]-x[i]*sum1[i]-c[i]

x=sum1[j], y=f[j]+sum2[j]x=sum1[j],\ y=f[j]+sum2[j].

则有

y=x[i]x+f[j]+sum2[i]x[i]sum1[i]c[i]y=x[i]*x+f[j]+sum2[i]-x[i]*sum1[i]-c[i]

直线的斜率为x[i]x[i], 截距为f[j]+sum2[i]x[i]sum1[i]c[i]f[j]+sum2[i]-x[i]*sum1[i]-c[i].

对于决策jj, 所抽象成的为点P(sum1[j], f[j]+sum2[j])P(sum1[j],\ f[j]+sum2[j]).

利用单调队列维护凸包即可.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e6 + 5;
typedef long long LL;
typedef double DB;
LL read() {
	LL x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
LL n, q[N];
DB x[N], p[N], c[N], sum1[N], sum2[N], f[N];
DB X(LL i) { return sum1[i]; }
DB Y(LL i) { return f[i] + sum2[i]; }
DB slope(LL i, LL j) { return (Y(i) - Y(j)) / (X(i) - X(j)); }
int main() {
	n = read();
	for(int i = 1; i <= n; ++ i) {
		x[i] = read(); p[i] = read(); c[i] = read();
		sum1[i] = sum1[i - 1] + p[i];
		sum2[i] = sum2[i - 1] + p[i] * x[i];
	}
	LL head = 1, tail = 1;
	for(int i = 1; i <= n; ++ i) {
		while(head < tail && slope(q[head], q[head+1]) < x[i]) head ++;
		int j = q[head];
		f[i] = f[j] + x[i] * (sum1[i] - sum1[j]) - (sum2[i] - sum2[j]) + c[i];
		while(head < tail && slope(i, q[tail-1]) < slope(q[tail-1], q[tail])) tail --;
		q[++ tail] = i;
	}
	printf("%lld\n", (LL)f[n]);
	return 0;
}

总结

(1).(1).写出DPDP方程, 判断能否使用斜率优化, 即是否存在与iijj分别相关的函数乘积.

(2).(2).根据不等式符号, f[i]f[i]的符号, Min/MaxMin/Max判断所维护的凸包的形状.

(3).(3).当斜率不单调时, 需要使用二分查找最优决策, 且要维护完整凸包.

当所加入点的横坐标不单调时, 需要使用CDQCDQ分治 或 平衡树维护凸包.

(4).(4).通常采用打表的方式确定是否具有决策单调性, 但打表并不能得到确切情况.

当不具有决策单调性(斜率不单调递增)时, 必须要保留整个凸包来确定完整的选择空间.

利用二分答案进行查找, 而不能直接取队首.

(5).(5).XX非严格递增时, 求斜率时可能会出现X(j1)==X(j2)X(j_1)==X(j_2).

此时最好不要直接return inf/infreturn\ inf/-inf, 否则可能会返回一个错误的极值(应结合图像返回极值).

而应return Y(j)Y(i) ? inf : infreturn\ Y(j) \geq Y(i)\ ?\ inf\ :\ -inf.

(6).(6).规范比较k0[i]k_0[i]slope(j1,j2)slope(j_1,j_2). 应用右面的点减去左面的点计算.

若写出如下不等式:

(X(j2)X(J1))k0Y(j2)Y(j1) / (X(j2)X(J1))k0Y(j2)Y(j1)(X(j_2)-X(J_1))*k_0\leq Y(j_2)-Y(j_1)\ /\ (X(j_2)-X(J_1))*k_0\geq Y(j_2)-Y(j_1)

若在继续计算过程中j1j_1j2j_2无意写反, 则会导致符号错误(相当于乘了一个1-1).

最好直接比较k0k_0Y(j2)Y(j1)X(j2)X(j1)\displaystyle \frac{Y(j_2)-Y(j_1)}{X(j_2)-X(j_1)}.

(7).(7).队列中初始化时应加入一个点P(0,0)P(0,0), 代表其决策点为00.

(8).(8).手写队列初始化为head=1,tail=0head=1,tail=0.

由于加入了初始点, 所以可以初始化head=tail=1head=tail=1.

(9).(9).手写队列判非空为headtailhead \leq tail.

由于出入队时应使用两个元素进行判断, 当队列中仅剩一个元素时, 不能进行判断.

队列判断存在至少两个元素的方法: head<tailhead<tail.

(10).(10).计算斜率时, 向下取整造成精度误差.

slopeslope函数最好设为long doublelong\ double类型(数据范围:1.21049321.2*10^{4932}).

(11).(11).在比较两个斜率时, 尽量写上==, 即,\leq,\geq. 而非<,><,>.

这样写可以去重(重点时斜率分母出问题).

四边形不等式

定义

设函数w(x, y)w(x,\ y)是定义在ZZ上的函数.

若对于a, b, c, dZ\forall a,\ b,\ c,\ d \in Z, 其中abcda \leq b \leq c \leq d.

若满足w(a, d)+w(b, c)w(a, c)+w(b, d)w(a,\ d)+w(b,\ c) \geq w(a,\ c)+w(b,\ d), 则称函数ww满足四边形不等式.

推论:

设函数w(x, y)w(x,\ y)是定义在ZZ上的函数.

若对于a, bZ\forall a,\ b \in Z, 其中a<ba < b.

若满足w(a, b+1)+w(a+1, b)w(a, b)+w(a+1, b+1)w(a,\ b+1)+w(a+1,\ b) \geq w(a,\ b)+w(a+1,\ b+1), 则称函数ww满足四边形不等式.

证明:

对于a<ca<c, 有:

w(a,c+1)+w(a+1,c)w(a,c)+w(a+1,c+1)w(a,c+1)+w(a+1,c) \geq w(a,c)+w(a+1,c+1)----------①

对于a+1<ca+1<c, 有:

w(a+1,c+1)+w(a+2,c)w(a+1,c)+w(a+2,c+1)w(a+1,c+1)+w(a+2,c) \geq w(a+1,c)+w(a+2,c+1)----------②

①+②并消项得:

w(a,c+1)+w(a+2,c)w(a,c)+w(a+2,c+1)w(a,c+1)+w(a+2,c) \geq w(a,c)+w(a+2,c+1)

依此类推, 对于abc\forall a \leq b \leq c, 则有(用bb代替a+2a+2, a+1<c, bc\because a+1<c,\ \therefore b \leq c):

w(a,c+1)+w(b,c)w(a,c)+w(b,c+1)w(a,c+1)+w(b,c) \geq w(a,c)+w(b,c+1)

同理, 对于abcd\forall a\leq b\leq c\leq d, 则有:

w(a,d)+w(b,c)w(a,c)+w(b,d)w(a,d)+w(b,c)\geq w(a,c)+w(b,d)

一维线性DP的四边形不等式优化

定理

对于形如f[i]=Min(f[j]+val(j,i))(0j<i)f[i]=Min(f[j]+val(j,i))(0\leq j<i)的式子, 若val(j,i)val(j,i)满足四边形不等式,

则函数f[i]f[i]具有决策单调性, 即对于函数f[i]f[i], 随着ii的增加, 取到最优解的jj单调不减.

证明:

设令f[i]f[i]取到最优解的jj值为p[i]p[i]. 满足i[1,n],p[i][0,i)j[0,p[i]]i\in[1,n],p[i]\in[0,i) j\in[0,p[i]].

根据最优解得:

f[p[i]]+val(p[i],i)f[j]+val(j,i)f[p[i]]+val(p[i],i)\leq f[j]+val(j,i)----------①

i[i+1,n]i'\in[i+1,n], 根据函数valval满足四边形不等式:

val(j,i)+val(p[i],i)val(j,i)+val(p[i],i)val(j,i')+val(p[i],i)\geq val(j,i)+val(p[i],i')

移项得

val(p[i],i)val(p[i],i)val(j,i)val(j,i)val(p[i],i')-val(p[i],i)\leq val(j,i')-val(j,i)----------②

①+②得

f[p[i]]+val(p[i],i)f[j]+val(j,i)f[p[i]]+val(p[i],i')\leq f[j]+val(j,i')

则对于ii'来说, 决策p[i]p[i]比决策jj更优.

f[i]f[i]具有决策单调性.

证毕.

例题

P1912 [NOI2009]诗人小G

题解:

f[i]f[i]为对前ii句诗进行排版的最小协调度, a[i]a[i]为每句诗的长度, sum[i]=k=1ia[i]sum[i]=\displaystyle \sum_{k=1}^{i}a[i].

f[i]=Min(f[j]+(sum[i]sum[j])+(ij1)Lp)(0j<i)f[i]=Min(f[j]+|(sum[i]-sum[j])+(i-j-1)-L|^p)(0\leq j<i)

方程带有高次项, 不好进行斜率优化或单调队列优化, 考虑决策单调性.

对于j<ij<i, 设val(j,i)=(sum[i]sum[j])+(ij1)Lpval(j,i)=|(sum[i]-sum[j])+(i-j-1)-L|^p.

要证明

val(j,i+1)+val(j+1,i)val(j,i)+val(j+1,i+1)val(j,i+1)+val(j+1,i)\geq val(j,i)+val(j+1,i+1)

移项得

val(j+1,i)val(j+1,i+1)val(j,i)val(j,i+1)val(j+1,i)-val(j+1,i+1)\geq val(j,i)-val(j,i+1)

u=(sum[i]sum[j])+(ij1)L=(sum[i]+i)(sum[j]+j)(L+1)u=(sum[i]-sum[j])+(i-j-1)-L=(sum[i]+i)-(sum[j]+j)-(L+1).

v=(sum[i]sum[j+1]+(i(j+1)1))L=(sum[i]+i)(sum[j+1]+j+1)(L+1)v=(sum[i]-sum[j+1]+(i-(j+1)-1))-L=(sum[i]+i)-(sum[j+1]+j+1)-(L+1).

即证:

vpv+(a[i+1]+1)pupu+(a[i+1]+1)p|v|^p-|v+(a[i+1]+1)|^p\geq |u|^p-|u+(a[i+1]+1)|^p

u>v\because u>v.

即证对于常数c>0c>0, xpx+cp|x|^p-|x+c|^p单调递减.

用导数可证此函数对于常数c(c>0)c(c>0)满足单调递减.

利用队列维护决策三元组即可.

code:

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long double LD;
const int N = 1e5 + 5;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
template <class T> T Abs(T a) { return a > 0 ? a : -a; }
int T, n, l, p, top, Last[N], Nxt[N];
LD sum[N], f[N];
string ch[N];
struct data { int l, r, p; } q[N];
LD ksm(LD x, int y) {
	LD res = 1;
	for( ; y; x = x * x, y >>= 1) {
		if(y & 1) res = res * x;
	}
	return res;
}
LD val(int j, int i) {
	return f[j] + ksm(Abs(sum[i] - sum[j] + (i-j-1) - l), p);
}
int find(data t, int x) {	//二分决策分界点; 
    int l = t.l, r = t.r;
    while(l <= r) {
        int mid = (l + r) >> 1;
        if(val(t.p, mid) < val(x, mid)) l = mid + 1;
        else r = mid - 1;
    }
    return l;
}
void solve() {
	int head = 1, tail = 0;
	q[++ tail] = (data) {0, n, 0};
	for(int i = 1; i <= n; ++ i) {
		if(head <= tail && i > q[head].r) head ++;	//去除不合法决策集合; 
		f[i] = val(q[head].p, i);	//i为最优决策, 计算f[i]; 
		Last[i] = q[head].p;	//Last保存 i 的最优决策来源; 
		if(head > tail || val(i, n) <= val(q[tail].p, n)) {
			//↓↓对于队尾集合左端点来说, i比 以前保存的最优决策 更优; 
			while(head <= tail && val(i, q[tail].l) <= val(q[tail].p, q[tail].l)) tail --; 
			if(head > tail) q[++ tail] = (data) {i, n, i};	//队列为空; 
			else  {
				int t = find(q[tail], i);	//找到最优位置; 
				q[tail].r = t - 1;	//插入新决策; 
				q[++ tail] = (data) {t, n, i};
			}
		}
	}
}
int main() {
	T = read();
	while(T --> 0) {
		n = read(); l = read(); p = read();
		for(int i = 1; i <= n; ++ i) cin >> ch[i];
		for(int i = 1; i <= n; ++ i) sum[i] = sum[i - 1] + ch[i].length();	//计算sum;
		solve();
		if(f[n] > (long long)1e18) puts("Too hard to arrange");
		else {
			printf("%lld\n", (long long)f[n]);
			for(int i = n; i; i = Last[i]) Nxt[Last[i]] = i;
			int now = 0;
			for(int i = 1; i <= n; ++ i) {
				now = Nxt[now];
				for(int j = i; j < now; ++ j) cout << ch[j] << " ";
				cout << ch[now] << "\n";
				i = now;
			}
		}
		puts("--------------------");
	}
	return 0;
}

二维区间DP的四边形不等式优化

定理1

对于a,b,c,dZ\forall a,b,c,d\in Z, 若函数ww满足四边形不等式, 且w(a,d)w(b,c)w(a,d) \geq w(b,c).

则函数ff也满足四边形不等式.

其中函数ff满足:

f(x,y)=Min(f(x,z)+f(z+1,y)+w(x,y)xz<y)f(x,y)=Min(f(x,z)+f(z+1,y)+w(x,y)|x\leq z<y)

特别的, 我们令f(x,y)=w(x,y)=0f(x,y)=w(x,y)=0.

证明:

x+1=yx+1=y时, 我们有:

f(x,y+1)+f(x+1,y)=f(x,x+2)+f(x+1,x+1)=f(x,x+2)f(x,y+1)+f(x+1,y)=f(x,x+2)+f(x+1,x+1)=f(x,x+2)

f(x,x+2)f(x,x+2)的最优决策为x+1x+1, 则有:

f(x,x+2)=f(x,x+1)+f(x+2,x+2)+w(x,x+2)=w(x,x+1)+w(x,x+2)f(x,x+2)=f(x,x+1)+f(x+2,x+2)+w(x,x+2)=w(x,x+1)+w(x,x+2)

w(x,x+1)+w(x,x+2)w(x,x+1)+w(x+1,x+2)w(x,x+1)+w(x,x+2)\geq w(x,x+1)+w(x+1,x+2)

f(x,x+2)f(x,x+2)的最优决策为xx, 则有:

f(x,x+2)=f(x,x)+f(x+1,x+2)+w(x,x+2)=w(x+1,x+2)+w(x,x+2)f(x,x+2)=f(x,x)+f(x+1,x+2)+w(x,x+2)=w(x+1,x+2)+w(x,x+2)

w(x+1,x+2)+w(x,x+2)w(x+1,x+2)+w(x,x+1)w(x+1,x+2)+w(x,x+2)\geq w(x+1,x+2)+w(x,x+1)

\because

w(x,x+1)+w(x+1,x+2)=f(x,x+1)+f(x+1,x+2)=f(x,y)+f(x+1,y+1)w(x,x+1)+w(x+1,x+2)=f(x,x+1)+f(x+1,x+2)=f(x,y)+f(x+1,y+1)

x+1=y\therefore x+1=y时, 我们得到:

f(x,y+1)+f(x+1,y)f(x,y)+f(x+1,y+1)f(x,y+1)+f(x+1,y)\geq f(x,y)+f(x+1,y+1)

即此时四边形不等式成立.

接下来, 我们运用数学归纳法:

假设当yx<ky-x<k时, 四边形不等式成立.

现在考虑yx=ky-x=k的情况:

f(x,y+1)f(x,y+1)aa为最优决策, f(x+1,y)f(x+1,y)bb为最优决策.

不妨设x+1a<bx+1 \leq a < b.

易得:

f(x,y+1)+f(x+1,y)=f(x,a)+f(a+1,y+1)+w(x,y+1)+f(x+1,b)+f(b+1,y)+w(x+1,y)f(x,y+1)+f(x+1,y)\\=f(x,a)+f(a+1,y+1)+w(x,y+1)+f(x+1,b)+f(b+1,y)+w(x+1,y)

对于f(x,y)f(x,y)f(x+1,y+1)f(x+1,y+1), 由于a,ba,b不一定为最优决策, 所以我们有:

f(x,y)+f(x+1,y+1)f(x,a)+f(a+1,y)+w(x,y)+f(x+1,b)+f(b+1,y+1)+w(x+1,y+1)f(x,y)+f(x+1,y+1)\\\leq f(x,a)+f(a+1,y)+w(x,y)+f(x+1,b)+f(b+1,y+1)+w(x+1,y+1)

\because函数ww满足四边形不等式,

w(x,y+1)+w(x+1,y)w(x,y)+w(x+1,y+1)\therefore w(x,y+1)+w(x+1,y)\geq w(x,y)+w(x+1,y+1)

根据归纳假设, 我们有:

f(a+1,y+1)+f(b+1,y)f(a+1,y)+f(b+1,y+1)f(a+1,y+1)+f(b+1,y)\geq f(a+1,y)+f(b+1,y+1)

于是, 我们有:

f(x,y+1)+f(x+1,y)f(x,y)+f(x+1,y+1)f(x,y+1)+f(x+1,y)\geq f(x,y)+f(x+1,y+1)

定理2

对于a,b,c,dZ\forall a,b,c,d\in Z, 若函数ww满足四边形不等式,且函数ff满足:

f(x,y)=Min(f(x,z)+f(z+1,y)+w(x,y)xz<y)f(x,y)=Min(f(x,z)+f(z+1,y)+w(x,y)|x\leq z<y)

特别的, 我们令f(x,y)=w(x,y)=0f(x,y)=w(x,y)=0.

P(x,y)P(x,y)f(x,y)f(x,y)取到最小值的kk值.

若函数ff满足四边形不等式, 则对于x,y\forall x,y, 我们有:

P(x,y1)P(x,y)P(x+1,y)P(x,y-1)\leq P(x,y)\leq P(x+1,y)

即函数f(x,y)f(x,y)具有决策单调性.

证明:

p=P(i,j)p=P(i,j).

对于x<kp\forall x<k\leq p, 由四边形不等式得:

f(x,t)+f(x+1,k)f(x,k)+f(x+1,t)f(x,t)+f(x+1,k)\geq f(x,k)+f(x+1,t)

移项得:

f(x+1,k)f(x+1,t)f(x,k)f(x,t)f(x+1,k)-f(x+1,t)\geq f(x,k)-f(x,t)

由于pp为最优决策, 所以我们有:

f(x,k)f(k+1,y)f(x,p)+f(p+1,y)f(x,k)-f(k+1,y)\geq f(x,p)+f(p+1,y)

所以:

(f(x+1,k)+f(k+1,y)+w(x+1,y))(f(x+1,p)+f(p+1,y)+w(x+1,y))=(f(x+1,k)f(x+1,p))+(f(k+1,y)f(p+1,y))(f(x,k)f(x,p))+(f(k+1,y)f(p+1,y))=(f(x,k)+f(k+1,y))(f(x,p)+f(p+1,y))0(f(x+1,k)+f(k+1,y)+w(x+1,y))-(f(x+1,p)+f(p+1,y)+w(x+1,y))\\=(f(x+1,k)-f(x+1,p))+(f(k+1,y)-f(p+1,y))\\\geq (f(x,k)-f(x,p))+(f(k+1,y)-f(p+1,y))\\=(f(x,k)+f(k+1,y))-(f(x,p)+f(p+1,y))\\\geq0

这意味着, 对于f(x+1,y)f(x+1,y)的任意决策kpk\leq p, pp都要比kk更优(包括相等)

所以:

P(x+1,y)P(x,y)P(x+1,y)\geq P(x,y)

同理可证:

P(x,y1)P(x,y)P(x,y-1)\leq P(x,y)

所以:

P(x,y1)P(x,y)P(x+1,y)P(x,y-1)\leq P(x,y) \leq P(x+1,y)

例题

新石子合并

题目大意:

nn堆石子围成一个圈, 每次可以选择相邻的两堆石子合并, 代价为两堆石子的总个数.

求最小代价和最大代价.

n5000n\leq 5000.

题解:

f1[i][j]f1[i][j]表示iijj之间石子合并的最小代价, f2[i][j]f2[i][j]表示iijj之间石子合并的最大代价.

对于f1f1, 我们可以列出状态转移方程:

f1[i][j]=Min(f[i][k]+f[k+1][j]+val(i,j))(ik<j)f1[i][j]=Min(f[i][k]+f[k+1][j]+val(i,j))(i\leq k<j)

其中, val(i,j)val(i,j)表示iijj之间的石子数目.

使用四边形不等式优化即可.

即对于f1[i][j]f1[i][j], 只需在区间[P[i][j1],P[i+1][j]][P[i][j-1],P[i+1][j]]枚举kk即可.

时间复杂度:O(n2)O(n^2).

最优决策P[i][j]P[i][j]打表程序见附加文件夹.

注意: 最大值并不能满足单调性, 但此时最大值满足一个性质.

对于f2[i][j]f2[i][j], 取到最优解的kk要么是ii, 要么是j1j-1.

证明:

利用反证法证明.

假设最优决策P[i][j]=pP[i][j]=p, 且i<p<j1i<p<j-1.

则存在两种情况.

(1).val(i,p)val(p+1,j)(1).val(i,p)\leq val(p+1,j).

t=P[i][p]t=P[i][p], 此时我们的方案为:

[i,i+1,i+2,,tt+1,t+2,,p]p+1,p+2,j[i,i+1,i+2,\dots ,t|t+1,t+2,\dots ,p]p+1,p+2,\dots j.

得分F1=(f[i][t]+f[t+1][p]+val(i,p))+f[p+1][j]+val(i,j)F_1=(f[i][t]+f[t+1][p]+val(i, p))+f[p+1][j]+val(i,j).

此时我们构造一种方案:

i,i+1,i+2,.t[t+1,t+2,,pp+1,p+2,j]i,i+1,i+2,\dots.t[t+1,t+2,\dots ,p|p+1,p+2,\dots j].

得分F2=f[i][t]+(f[t+1][p]+f[p+1][j]+val(t+1,j))+val(i,j)F_2=f[i][t]+(f[t+1][p]+f[p+1][j]+val(t+1,j))+val(i,j).

t<p, val(i,p)val(p+1,j)<val(t+1,j)\because t<p,\ \therefore val(i,p)\leq val(p+1,j)<val(t+1,j).

F1<F2\therefore F_1<F_2, 即决策pp并不是最优决策.

(2).val(i,p)>val(p+1,j)(2).val(i,p)>val(p+1,j).

t=P[p+1][j]t=P[p+1][j], 此时我们的方案为:

i,i+1,i+2,p[p+1,p+2,,tt+1,t+2,j]i,i+1,i+2,\dots p[p+1,p+2,\dots ,t|t+1,t+2,\dots j].

得分F1=f[i][p]+(f[p+1][t]+f[t+1][j]+val(p+1,j))+val(i,j)F_1=f[i][p]+(f[p+1][t]+f[t+1][j]+val(p+1,j))+val(i,j).

此时我们构造一种方案:

[i,i+1,i+2,,pp+1,p+2,,t]t+1,t+2,j[i,i+1,i+2,\dots ,p|p+1,p+2,\dots,t]t+1,t+2,\dots j.

得分F2=(f[i][p]+f[p+1][t]+val(i,t+1))+f[t+1][j]+val(i,j)F_2=(f[i][p]+f[p+1][t]+val(i,t+1))+f[t+1][j]+val(i,j).

t+1>p, val(i,t+1)>val(i,p)>val(p+1,j)\because t+1>p,\ \therefore val(i,t+1)>val(i,p)>val(p+1,j).

F1<F2\therefore F_1<F_2, 即决策pp不是最优决策.

与假设矛盾, 因此最优决策只可能是ii或者j1j-1.

证毕.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e3 + 5;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int n, a[N], sum[N], f1[N][N], f2[N][N], p[N][N];
int main(){
    n = read();
    for(int i = 1; i <= n; ++ i) a[i + n] = a[i] = read();
	for(int i = 1; i <= (n << 1); ++ i) {
		sum[i] = sum[i - 1] + a[i]; p[i][i] = i;
	}
	for(int i = (n << 1); i; -- i) {
		for(int j = i + 1; j <= (n << 1); ++ j) {
			f1[i][j] = 1e9;
			f2[i][j] = Max(f2[i][i] + f2[i + 1][j], f2[i][j - 1] + f2[j][j]) + sum[j] - sum[i - 1];
			for(int k = p[i][j - 1]; k <= p[i + 1][j]; ++ k) {
				if(f1[i][k] + f1[k + 1][j] + sum[j] - sum[i - 1] < f1[i][j]) {
					f1[i][j] = f1[i][k] + f1[k + 1][j] + sum[j] - sum[i - 1];
					p[i][j] = k;
				}
				else if(f1[i][k] + f1[k + 1][j] + sum[j] - sum[i - 1] == f1[i][j]) {
					p[i][j] = Max(p[i][j], k);
				}
			}
		}
	}
	int ans = 1e9;
	int ans2 = 0;
	for(int i = 1; i <= n; ++ i) ans = Min(ans, f1[i][i + n - 1]);
	for(int i = 1; i <= n; ++ i) ans2 = Max(ans2, f2[i][i + n - 1]); 
	printf("%d\n%d\n", ans, ans2);
	return 0;
}

虚树

定义

对于一棵节点数为nn的树TT.

构造一棵树TT'使得总结点数最少, 且包含指定的某些节点和它们的LCALCA.

TT'即为树TT虚树.

构造方法

前置知识:dfsdfs序, lcalca.

预处理整棵树的dfsdfs序及lcalca.

假设我们要构造kk个指定点的虚树.

先将这kk个点按照dfsdfs序从小到大排序, 我们将把这些点逐一加入到虚树中.

为方便后面的处理, 我们可以先强行加入根节点.

用一个栈stack[ ]stack[\ ]保存虚树上从根出发到最后加入虚树的一条路径上所经过的点(按深度从小到大存储).

当虚树中加入点a[k]a[k]后.

满足sta[1]=root,sta[top]=a[k]sta[1]=root,sta[top]=a[k](栈中保存了从根到a[k]a[k]路径上的点).

sta[x]sta[x1](2xtop)sta[x]为sta[x-1](2\leq x\leq top)的子结点.

考虑新加入一个点xx, 设z=lca(x,sta[top])z=lca(x,sta[top]), 分情况讨论.

(1).z=sta[top](1).z=sta[top]. 即xxstack[top]stack[top]子树内的点.

直接sta[++top]=xsta[++top]=x即可.

(2).zsta[top](2).z\neq sta[top].
此时xx一定不是sta[top]sta[top]子树内的点.

注:sta[top]>sta[top1]sta[top]->sta[top-1]z>xz->x在原树上不一定直接连接.

我们不断弹栈, 直到dep[sta[top1]]<dep[z]dep[sta[top-1]] < dep[z].

弹栈时在虚树上连接sta[top1]>sta[top]sta[top-1]->sta[top].

此时, sta[top]sta[top]不一定为zz, 我们需要将zz加入到虚树中维护.

当虚树构建完成后, 栈应该为空.

不断弹栈, 连接sta[top1]>sta[top]sta[top-1]->sta[top].

具体分析

先将整个树dfsdfs一遍, 求出每个节点的dfsdfs序 + depdep.

假如原树如下图:

我们要建立以黑色节点为关键点的虚树.

标出dfsdfs序, 为防止图片混乱, 未标明depdep, 如下图.

将关键点按照dfsdfs序从小到大排序.

设栈顶元素为pp, 现在要插入的关键点为xx, lca=lca(p,x)lca=lca(p,x), 分情况讨论.

(1).lca=p(1).lca=p, 如下图.

(2).p,x(2).p,x分别位于lcalca的两棵子树, 如下图.

考虑为何lcaxlca\neq x.

我们对关键点按照dfsdfs序进行遍历.

根据dfsdfs序的性质.

对于任意两个点x,y(dfn[x]>dfn[y])x,y(dfn[x]>dfn[y]), 都存在dfn[lca]dfn[x],dfn[lca]dfn[y]dfn[lca]\leq dfn[x],dfn[lca]\leq dfn[y].

x=lcax=lca, 则有dfn[x]dfn[y]dfn[x]\leq dfn[y]. 情况矛盾, 不成立.

考虑上面所提到的情况(1)(1).

本来维护(下文中数字为图中dfsdfs序)121-2链.

由于lca=plca=p,因此现在要维护1241-2-4链.

直接压栈即可.

例如压栈后, 栈内(dfsdfs序)情况, 如下图:

在维护下图的链(默认根节点在栈内):

考虑较为复杂的情况(2)(2).

注意要循环处理情况(2)(2).

设栈内第二个元素sta[top1]sta[top-1]qq, 分情况讨论:

①.dfn[q]>dfn[lca]dfn[q]>dfn[lca].

图中的信息:

qq为根的子树已经遍历完毕, 现在进入到以xx为根的子树.

现在需要把以qq为根的子树信息保存好.

当遍历到pp点时, 栈中的维护情况,如下图:

即维护了12341-2-3-4的链, 如下图:

当前要维护的链, 如下图:

当我们弹出点pp时, 等同于失去了q>pq->p的信息.

因此在虚树上连边q>pq->p.

所以, 当dfn[q]>dfn[lca]dfn[q]>dfn[lca]时, 虚树上连边q>pq->p, 退栈一次.

②.dfn[q]=dfn[lca]dfn[q]=dfn[lca].

我们接着上幅图, 退栈一次后情况如下图(紫色边为虚树上的边):

我们需要连接lca>plca->p来维护左子树, 然后要维护右子树的链.

pp弹栈后, 将xx入栈, 终止循环.

所以当dfn[q]=dfn[lca]dfn[q]=dfn[lca]时, 由lcalcapp连边, 退栈后, 将点xx入栈, 终止循环.

③.dfn[q]<dfn[lca]dfn[q]<dfn[lca], 情况如下图:

栈顶元素为pp, 第二个元素为qq(根节点最开始就位于栈内).

dfn[q]<dfn[lca]dfn[q]<dfn[lca]时, 思考其中包含的信息.

a.pa.plcalca之间不存在关键点, 因为最近的关键点为qq.

所以lcalca的左子树需要连接lca>plca->p, 同时退栈一次.

b.lcab.lca不在栈中.即lcalca不是关键点, 但因为lcalca连接着点pp和点xx的关系.

因此, 我们将lcalca也加入栈中.

所以, 当dfn[q]<dfn[lca]dfn[q]<dfn[lca]时, 虚树上连接lca>plca->p, 退栈一次, 将lca,xlca, x依次入栈, 终止循环.

最后, 若栈内还有元素未弹出, 应将栈内元素依次连边.

图解虚树建立过程

对于一开始的图, 我们模拟它的构建过程, 初始图:

(1).(1).插入根节点, 无根树转有根树.

根节点入栈, 如下图:

(2).(2).插入33节点.

lca(3,1)=1lca(3,1)=1, 满足第一种情况, 直接压栈, 维护链131-3.

此时栈内情况如下图:

(3).(3).插入55节点.

lca(3,5)=2lca(3, 5)=2, 发现p,xp,x位于不同的子树中, 求出q=1q=1, 如下图:

我们发现dfn[q]<dfn[lca]dfn[q]<dfn[lca], 根据上文提到的做法.

在虚树上连边lca>plca->p, 退栈, 将lca,xlca,x依次压入栈中.

即本来维护链131-3, 现在维护链1251-2-5.

树和栈中的情况分别如下图:

(4).(4).插入88节点.

lca(5,8)=1lca(5,8)=1, 求出q=2q=2.发现dfn[q]>dfn[lca]dfn[q]>dfn[lca].

xx点位于一个新的子树中, 因此我们应维护新的链, 并且要保存原来的链.

连接q>pq->p, 退栈.

此时p=2,q=1p=2,q=1,且dfn[q]=dfn[lca]dfn[q]=dfn[lca],满足上文第二种情况.

连接lca>plca->p, 退栈, 将xx压入栈中, 维护新的链181-8, 终止循环.

完成后如下图:

(5).(5).插入99节点

lca(8,9)=8lca(8,9)=8, 满足lca(p,x)=plca(p,x)=p的情况.

直接将99压栈维护, 此时维护的链为1891-8-9.

(6).(6).插入1010节点.

lca(9,10)=1lca(9,10)=1, 两个节点位于lcalca的两棵不同子树中, 求出q=8q=8.

发现dfn[q]>dfn[lca]dfn[q]>dfn[lca], 连接q>pq->p, 退栈.

此时p=8,q=1,lca=1p=8,q=1,lca=1.

发现dfn[q]=dfn[lca]dfn[q]=dfn[lca], 所以连接lca>plca->p, 退栈, 将1010入栈, 如下图.

(7).(7).清空栈内剩余元素.

我们依次由sta[top1]sta[top-1]sta[top]sta[top]连边, 即连接1>101->10.

完成后如下:

将紫色的边提出后形成的树即为虚树, 如下图所示:

code:

void Build_Virtual_Tree (int k) {
	sort(a + 1, a + k + 1, cop);//按 dfs 序从小到大排序;
	if(a[1] != 1) sta[top = 1] = 1;//默认根节点为关键点;
	for(int i = 1; i <= k; ++ i) {
		int x = a[i];
        //↓↓点1不会出栈(最后才会特殊处理), 因此top=1时可以直接加入点(lca(1,x)=1,一定会直接入栈).
		if(top <= 1) { sta[++ top] = x; continue; }
		int lca = LCA(sta[top], x);
		if(lca == sta[top]) { sta[++ top] = x; continue; }	//情况(1), 直接加入;
		while((top > 1) && dep[sta[top - 1]] >= dep[lca]) Add(sta[top - 1], sta[top]), top --;//循环处理(2)的①;
		if(lca != sta[top]) Add(lca, sta[top]), sta[top] = lca;//情况(2)的③. 
		sta[++ top] = x;
	}

时间复杂度

每个指定点进出栈一次, O(k)O(k).

例题

CF613D Kingdom and its Cities

题目大意:

给定一棵树, 每组询问给定kk个点, 你可以删掉不同于那kk个点的mm个点, 使得这kk个点两两不连通, 要求最小化mm, 如果不可能输出1-1.询问之间独立.多组询问.

n105,k105n\leq 10^5,\sum k\leq 10^5.

题解:

对于每组询问, 构建虚树, 在虚树上进行DPDP.

特判无解: 存在某个点和它父亲都被指定.

将被指定的点tagtag设成11, 然后dfsdfs这颗虚树.

假设当前dfsdfs到点uu.

(1).(1).xx为被指定的点.

若存在子结点vvtag0tag\neq 0, 即u>vu->v出现连通.

此时在u>vu->v的路径上(不包括u,vu,v)随便删除一点(++ans++ans)即可.

若所有子结点tag=0tag=0, 则不用进行处理.

(2).(2).xx为未被指定的点.

统计有多少个子结点tag0tag\neq 0.

若只有一个, 则可以尽量不删除点, 但因为xx是未被指定的点(tag=0tag=0), 因此将xxtagtag设为11.

若超过一个, 此时若不删除点, 就会存在连通.

xx点是未被指定的点, 所以可以删除点xx(++ans++ans).

若对于每个询问memset()memset(), 复杂度会退化到nqnq.

树链剖分求lcalca, 时间复杂度:O(n+klog2n)O(n+\sum klog_2n).

倍增/STST表求lcalca, 时间复杂度:O(n+nlog2n)O(n+nlog_2n).

多组数据, 因此在每组数据中最后一遍dfsdfs虚树时, 应将边清空.

方式:修改头指针, 若为vectorvector, 直接clearclear即可.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int n, m;
struct Edge {
	int to;
	Edge* nxt;
	Edge(int to, Edge* nxt) : to(to), nxt(nxt) {}
} *head[N];
void add(int x, int y) {
	head[x] = new Edge(y, head[x]);
}
vector<int> v[N];
void Add(int x, int y) { v[x].push_back(y); }
int a[N], dfn[N], Top[N], siz[N], son[N], sta[N];
int top, dep[N], fa[N], tot, ans, tag[N];
int cop(int x, int y) { return dfn[x] < dfn[y]; }
void dfs1(int x, int f) {
	dep[x] = dep[fa[x] = f] + (siz[x] = 1);
	for(Edge* i = head[x]; i; i = i->nxt) {
		if(i->to == f) continue;
		dfs1(i->to, x);
		siz[x] += siz[i->to];
		if(siz[i->to] > siz[son[x]]) son[x] = i->to;
	}
}
void dfs2(int x, int tp) {
	Top[x] = tp; dfn[x] = ++ tot;
	if(!son[x]) return;
	dfs2(son[x], tp);
	for(Edge* i = head[x]; i; i = i->nxt) {
		if(Top[i->to] == 0) {
			dfs2(i->to, i->to);
		}
	}
}
int LCA(int x, int y) {
	while(Top[x] != Top[y]) {
		if(dep[Top[x]] < dep[Top[y]]) swap(x, y);
		x = fa[Top[x]];
	}
	return dep[x] < dep[y] ? x : y;
}
void Build_Virtual_Tree (int k) {
	sort(a + 1, a + k + 1, cop);
	if(a[1] != 1) sta[top = 1] = 1;
	for(int i = 1; i <= k; ++ i) {
		int x = a[i];
		if(top <= 1) { sta[++ top] = x; continue; }
		int lca = LCA(sta[top], x);
		if(lca == sta[top]) { sta[++ top] = x; continue; }
		while((top > 1) && dep[sta[top - 1]] >= dep[lca]) Add(sta[top - 1], sta[top]), top --;
		if(lca != sta[top]) Add(lca, sta[top]), sta[top] = lca;
		sta[++ top] = x;
	}
	while(top) Add(sta[top - 1], sta[top]), top --;
}
void DP(int x) {
	if(tag[x]) {	//x是关键点;
		for(int i = 0; i < (int)v[x].size(); ++ i) {
			int to = v[x][i]; DP(to);
			if(tag[to]) tag[to] = 0, ans;	//若不删边, 则会有连通;
		}
	}
	else {	//x不是关键点;
		for(int i = 0; i < (int)v[x].size(); ++ i) {
			int to = v[x][i]; DP(to);
			tag[x] += tag[to];	//将关键点个数累加到父亲节点上;
			tag[to] = 0;	//清零,不能memset,否则复杂度退化;
		}
		if(tag[x] > 1) tag[x] = 0, ans ++;	//若不删边, 则会有连通;
	}
	v[x].clear();
}
signed main() {
	n = read();
	for(int i = 1, x, y, z; i < n; ++ i) {
		x = read(); y = read();
		add(x, y); add(y, x);
	}
	dfs1(1, 0); dfs2(1, 1);
	m = read();
	while(m --> 0) {
		int k = read();
		for(int i = 1; i <= k; ++ i) tag[a[i] = read()] = 1;
		int flag = 0;
		for(int i = 1; i <= k; ++ i) {
			if(tag[fa[a[i]]]) { puts("-1"); flag = 1; break; }
		}
		if(flag == 1) {
			for(int i = 1; i <= k; ++ i) tag[a[i]] = 0;
			continue;
		}
		ans = 0;
		Build_Virtual_Tree(k);
		DP(1);
		printf("%lld\n", ans);
		tag[1] = 0;
	}
	return 0;
}

P2495 [SDOI2011]消耗战

题目大意:

给出一棵树, 每条边有边权.

mm次询问, 每次询问给出kk个点, 问使得这kk个点均不与1号点(根节点)相连的最小代价.

n250000,m1,k5105n\leq 250000, m\geq 1, \sum k \leq 5*10^5.

题解:

对于每组询问, 在虚树上DPDP.

f[x]f[x]xx为根的子树全部与根节点断开的最小代价.

minl[i]minl[i]表示根节点到ii路径上的最小边权.

特殊的, minl[1]=infminl[1]=inf.

设边EdgeEdge上的两点x,y(dep[x]<dep[y])x,y(dep[x]<dep[y]).

minl[y]=Min(minl[x],i>val)minl[y]=Min(minl[x],i->val).

f[x]=Min(Minl[x],kson[x]Minl[k])f[x]=Min\Bigg (Minl[x],\displaystyle \sum_{k\in son[x]}Minl[k]\Bigg).

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int n, m;
struct Edge {
	int to, val;
	Edge* nxt;
	Edge(int to, int val, Edge* nxt) : to(to), val(val), nxt(nxt) {}
} *head[N];
void add(int x, int y, int z) {
	head[x] = new Edge(y, z, head[x]);
}
vector<int> v[N];
void Add(int x, int y) { v[x].push_back(y); }
int a[N], dfn[N], Top[N], siz[N], son[N], sta[N];
int top, dep[N], fa[N], tot, minl[N];
int cop(int x, int y) { return dfn[x] < dfn[y]; }
void dfs1(int x, int f) {
	dep[x] = dep[fa[x] = f] + (siz[x] = 1);
	for(Edge* i = head[x]; i; i = i->nxt) {
		if(i->to == f) continue;
		minl[i->to] = min(minl[x], (LL)i->val);
		dfs1(i->to, x);
		siz[x] += siz[i->to];
		if(siz[i->to] > siz[son[x]]) son[x] = i->to;
	}
}
void dfs2(int x, int tp) {
	Top[x] = tp; dfn[x] = ++ tot;
	if(!son[x]) return;
	dfs2(son[x], tp);
	for(Edge* i = head[x]; i; i = i->nxt) {
		if(Top[i->to] == 0) {
			dfs2(i->to, i->to);
		}
	}
}
int LCA(int x, int y) {
	while(Top[x] != Top[y]) {
		if(dep[Top[x]] < dep[Top[y]]) swap(x, y);
		x = fa[Top[x]];
	}
	return dep[x] < dep[y] ? x : y;
}
void Build_Virtual_Tree (int k) {
	sort(a + 1, a + k + 1, cop);
	if(a[1] != 1) sta[top = 1] = 1;
	for(int i = 1; i <= k; ++ i) {
		int x = a[i];
		if(top <= 1) { sta[++ top] = x; continue; }
		int lca = LCA(sta[top], x);
		if(lca == sta[top]) continue;	//特殊判断,关键点以下的关键点无用,若加入虚树中后,DP有误.
		while((top > 1) && dep[sta[top - 1]] >= dep[lca]) Add(sta[top - 1], sta[top]), top --;
		if(lca != sta[top]) Add(lca, sta[top]), sta[top] = lca;
		sta[++ top] = x;
	}
	while(top) Add(sta[top - 1], sta[top]), top --;
}
int DP(int x) {
	if(v[x].size() == 0) return minl[x];
	int sum = 0;
	for(int i = 0; i < v[x].size(); ++ i) {
		sum += DP(v[x][i]);
	}
	v[x].clear();
	return Min(minl[x], sum);//当点x为关键点时,返回值一定为min[x],故不用分类讨论;
}
signed main() {
	freopen("1.in", "r", stdin);
	freopen("a.out", "w", stdout);
	n = read();
	for(int i = 1, x, y, z; i <= n - 1; ++ i) {
		x = read(); y = read(); z = read();
		add(x, y, z); add(y, x, z);
	}
	minl[1] = 1e18;
	dfs1(1, 0); dfs2(1, 1);
	m = read();
	while(m --> 0) {
		int k = read();
		for(int i = 1; i <= k; ++ i) a[i] = read();
		Build_Virtual_Tree(k);
		printf("%lld\n", DP(1));
	}
	return 0;
}

斯坦纳树

引言

给定一张图, 若要求某几个点的生成树, 而不强制要求其点连通(可以连通)的代价.

常见的问题有最小斯坦纳树Minimal Steiner Tree(MST)Minimal\ Steiner\ Tree(MST)

实际上, 斯坦纳树常用于解决一类没有多项式解法的最小生成树问题.

斯坦纳树使用状压DPDP求解这类问题.

例题

P4294 [WC2008]游览计划

题解:

最终形成的图一定没有环, 且一定都互相连通,

即最终形成的图为一个树.

最小斯坦纳树求解通法:

f[i][sta]f[i][sta]表示考虑到以ii节点为根, 且与之相连的景点的状态为stasta的最小代价.

stasta为二进制数, 00表示不连通, 11表示联通.

初始化: f[i][1<<(i1)]=0f[i][1<<(i-1)]=0.

转移有两种情况:

  • stasta的子集ss转移过来

考虑合并两个连通块, 要减去一个ii的权值(防止加重)

f[i][sta]=Min(f[i][s]+f[i][sta\mbox{^}s]-val[i])

转移可以通过二进制枚举来实现.

  • 由不含该点的状态转移过来

ii为新加入的点(i, ki,\ k相邻).

f[i][j]=Min(f[k][j]+val[i])f[i][j]=Min(f[k][j]+val[i])

该转移没有明确的转移顺序, 因为i,ki,k相邻时, 状态可以互相转化.

即对于上文的方程, 我们还可以反过来转移:

f[k][j]=Min(f[i][j]+val[k])f[k][j]=Min(f[i][j]+val[k])

对于这类问题的转移, 可以类比最短路dis[i]=Min(dis[j]+val(i,j))dis[i]=Min(dis[j]+val(i,j)),

同样都是可以反过来转移: dis[j]=Min(dis[i]+val(j,i))dis[j]=Min(dis[i]+val(j,i)).

但究其本质, 当最短路完成转移时, 实际上是每条边都满足三角形不等式.

即对于任意一条边len=val(i,j)len=val(i,j), 都满足dis[i]dis[j]+lendis[i]\leq dis[j]+len.

若不满足三角形不等式, 则dis[i]dis[i]一定不是最优状态.

因此可得, 这类问题可以利用SPFASPFA不断松弛优化转移.

时间复杂度:O(n3k+cE2k)O(n*3^k+cE*2^k).

ccSPFASPFA复杂度中的常数, EE为边的数量, 但几乎达不到全部边的数量, 甚至非常小,

3k3^k来自于子集的转移i=1nC(i,n)2i\displaystyle \sum_{i=1}^n C(i,n)*2^i, 二项式展开求和.

此题中将ii转化为二维的点即可.

输出方案的话直接记录路径, 记录从哪种方式的哪种状态转移过来, dfsdfs一遍即可.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define fi first
#define se second
using namespace std;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int n, m, tot = 0;
int dx[5] = {-1, 1, 0, 0};
int dy[5] = {0, 0, -1, 1};
int vis[12][12], a[12][12], f[12][12][1050];
struct node { int x, y, s; } e[12][12][1050];
queue <pair<int, int> > q;
void spfa(int cur) {
	while(! q.empty()) {
		pair<int, int> p = q.front(); q.pop();
		vis[p.fi][p.se] = 0;
		for(int i = 0; i <= 3; ++ i) {
			int wx = p.fi + dx[i], wy = p.se + dy[i];
			if(wx < 1 || wx > n || wy < 1 || wy > m) continue;
			if(f[wx][wy][cur] > f[p.fi][p.se][cur] + a[wx][wy]) {
				f[wx][wy][cur] = f[p.fi][p.se][cur] + a[wx][wy];
				e[wx][wy][cur] = (node) {p.fi, p.se, cur};	//记录最优解的状态转移;
				if(vis[wx][wy] == 0) {
					vis[wx][wy] = 1; q.push(make_pair(wx, wy));
				}
			}
		}
	}
}
void dfs(int x, int y, int now) {
	vis[x][y] = 1;
	node tmp = e[x][y][now];
	if(tmp.x == 0 && tmp.y == 0) return;	//搜索边界;
	dfs(tmp.x, tmp.y, tmp.s);	//缩小搜索范围
	if(tmp.x == x && tmp.y == y) {	//若最优解为枚举子集转移, 还要搜索另一个子集;
		dfs(tmp.x, tmp.y, now - tmp.s);
	}
}
int main() {
	n = read(); m = read();
	memset(f, 0x3f, sizeof(f));
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= m; ++ j) {
			a[i][j] = read();
			if(a[i][j] == 0) f[i][j][1<<tot] = 0, tot ++;	//初始化;
		}
	}
	int sum = (1 << tot) - 1;
	for(int sta = 0; sta <= sum; ++ sta) {	//枚举状态;
		for(int i = 1; i <= n; ++ i) {	//枚举点;
			for(int j = 1; j <= m; ++ j) {
				for(int s = sta; s; s = (s - 1) & sta) {	//枚举子集转移;
					if(f[i][j][sta] > f[i][j][s] + f[i][j][sta^s] - a[i][j]) {
						f[i][j][sta] = f[i][j][s] + f[i][j][sta^s] - a[i][j];
						e[i][j][sta] = (node) {i, j, s};	//结构体 e 记录最优解的来源;
					}
				}
				if(f[i][j][sta] < 2e9) {	//预先加入到队列中;
					q.push(make_pair(i, j)), vis[i][j] = 1;
				}
			}
		}
		spfa(sta);	//对所枚举的状态松弛转移;
	}
	int s, t, flag = 0;
	for(int i = 1; i <= n; ++ i) {	//找到一个景点位置(s,t);
		for(int j = 1; j <= m; ++ j) {
			if(a[i][j] == 0) {
				s = i; t = j;
				flag = 1; break;
			}
		}
		if(flag == 1) break;
	}
	printf("%d\n", f[s][t][sum]);	//输出最小斯坦纳树;
	memset(vis, 0, sizeof(vis));
	dfs(s, t, sum);
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= m; ++ j) {
			if(a[i][j] == 0) putchar('x');	//景点;
			else if(vis[i][j]) putchar('o');	//选择了这条路;
			else putchar('_');	//未选择这条路;
		}
		puts("");
	}
	return 0;
}

P3264 [JLOI2015]管道连接

题目大意:

给出一张nn个点mm条边的无向图和pp个特殊点, 每个特殊点有一个颜色cic_i.

要求选出若干条边, 使得颜色相同的特殊点在同一个连通块内. 输出最小边权和.

0<cip10,0<n103,0m30000<c_i\leq p\leq 10,0<n\leq 10^3,0\leq m\leq 3000.

题解:

斯坦纳树 + 状压DPDP.

先求出斯坦纳树.

对于本题, 选出的连通块不一定只有一个, 不能直接得到答案.

状压DPDP, 设g[i]g[i]表示状态为ii的颜色满足同颜色的点全部连通的最小代价.

若只存在一个连通块, 则答案为斯坦纳树的f[i][sta]f[i][sta].

若存在多连通块, 则枚举子集进行转移即可.

时间复杂度:O(3pn+cE2p)O(3^p*n+c*E*2^p).

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#define RI register int 
using namespace std;
const int N = 1030;
inline int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
queue <int> q;
int n, m, p, c[N], d[N], g[N], vis[N][N], f[N][N];
struct Edge {
	int to, val;
	Edge* nxt;
	Edge(int to, int val, Edge* nxt) : to(to), val(val), nxt(nxt) {};
} *head[N<<3];
void add(int x, int y, int z) { head[x] = new Edge(y, z, head[x]); }
int main() {
	n = read(); m = read(); p = read();
	memset(f, 0x3f, sizeof(f));
	for(RI i = 1, x, y, z; i <= m; ++ i) {
		x = read(); y = read(); z = read();
		add(x, y, z); add(y, x, z);
	}
	for(RI i = 1; i <= p; ++ i) {
		c[i] = read(); d[i] = read();
		f[d[i]][1 << (i - 1)] = 0;
	}
	for(RI i = 1; i <= n; ++ i) f[i][0] = 0;
	for(RI sta = 1; sta < (1<<p); ++ sta) {
		for(RI s = sta; s; s = (s - 1) & sta) {
			for(RI k = 1; k <= n; ++ k) {
				f[k][sta] = Min(f[k][sta], f[k][s] + f[k][sta ^ s]);
			}
		}
		for(RI j = 1; j <= n; ++ j) q.push(j), vis[j][sta] = 1;
		while(! q.empty()) {
			int x = q.front(); q.pop();
			vis[x][sta] = 0;
			for(Edge* j = head[x]; j; j = j->nxt) {
				if(f[j->to][sta] > f[x][sta] + j->val) {
					f[j->to][sta] = f[x][sta] + j->val;
					if(vis[j->to][sta] == 0) q.push(j->to); 
				}
			}
		}
	}
	memset(g, 0x3f, sizeof(g));
	for(int i = 1; i < (1 << p); ++ i) {	//二进制枚举颜色,(颜色状态上限为(1<<p)-1);
		int k = 0;	//k保存二进制特殊点;
		for(RI j = 1; j <= p; ++ j) {	//枚举特殊点, 计算k;
			if(i & (1 << (c[j] - 1))) k |= (1 << (j - 1));
		}
		for(RI j = 1; j <= n; ++ j) g[i] = Min(g[i], f[j][k]);	//考虑全集;
		for(RI j = i; j; j = (j - 1) & i) g[i] = Min(g[i], g[j] + g[i ^ j]);	//考虑子集;
	}
	printf("%d\n", g[(1 << p) - 1]);
	return 0;
}

插头DP

简介

下面是你要的插头, 至于你说的插头DP是什么?

呃\dots

插头DP为"基于连通性状态压缩的动态规划问题"的简称, 最早出现在陈丹琦的论文中.

基本概念

插头

代指两个格子之间的连通性.

DPDP的某一个状态中, 某相邻的两个格子为连通的(如:处于同一条路径上, 被同一个矩形覆盖\dots), 那么我们认为这两个格子之间存在插头.

如图, 格子(1,1)(1, 1)与格子(2,1)(2, 1)连通,则称格子(1,1)(1, 1)有一个下插头,格子(2,1)(2, 1)有一个上插头.

每个格子所拥有的插头数量因题目而异.

如下图, 一条回路的插头表示:

轮廓线

轮廓线为一条分割线.

作用为分隔开DPDP已经覆盖的状态和暂未覆盖的状态, 轮廓线是在插头DP的棋盘模型中被定义的.

插头DPDP的两种DPDP方式: 逐行递推, 逐格递推.

逐行递推的方式在很多题目中状态过多, 因此一般会使用逐格递推.

下文的讲解都是基于逐格递推.

逐格递推的轮廓线:

设当前正在递推格子(i, j)(i,\ j).

那么轮廓线即为一条从棋盘左边界开始,沿着第i行的格子底部向右延伸至格子(i,j)(i,j)的左下角,

然后向上一格,再一直向右延伸到棋盘右边界的一条线.

轮廓线的长度为m(\mbox{列数}) + 1.

黄色格子为正在递推的格子(由黄色格子左面的格子向黄色格子转移).

红色的线为轮廓线.

轮廓线的上方有mm个下插头位置和一个右插头位置.

轮廓线的实际作用:存储状态.

棋盘模型

给定一个棋盘(约为151515*15), 有些格子可能为障碍.

求使用闭合路径或一些特殊形状的图案覆盖棋盘上的每一个格子的 最优回路 / 方案数.

题目中的关键点:

把轮廓线画出来以后,轮廓线上方的会是一些完备的“题目要求的形状”和一些“一头扎到”轮廓线下面,

也就是被轮廓线切割的形状,而这些被切割的形状在轮廓线上方的分布我们不清楚,

但是我们可以用轮廓线上插头的状态来表示所有情形的和.

例如:

现在有一条轮廓线, 轮廓线上面有一些插头.

我们现在可能不知道轮廓线上面的情况具体是怎么样的, 但是我们根据这些插头就能往下推下面的状态.

所以我们只要记录这种插头状态下轮廓线上方的答案, 然后往下推就好了.

即 将dfsdfs找覆盖的问题 转化 成了dpdp模型.

从插头DP的棋盘模型中, 引出插头DP中棋盘模型的状态定义:

f[i][j][s]f[i][j][s]为当前递推到格子(i, j)(i,\ j), 轮廓线状态为 ss 的方案数.

例题

P5056 【模板】插头dp

题目大意:

给定一张nmn*m的网格图, 求经过所有非障碍点一次的回路个数.

2n,m122\leq n,m\leq12.

题解:

插头DPDP: 我们使用逐格递推.

f[i][j][s]f[i][j][s]表示当前递推到格子(i,j)(i,j), 轮廓线状态为ss的方案数.

考虑我们在递推到某一个格子时, 这个格子和下一个格子所对应的轮廓线.

假设我们递推到某一个格子时的轮廓线为下图:

则对于下一个格子来说, 其轮廓线为:

观察这两个轮廓线, 发现仅有两个位置不同.

我们从上一格的状态推到下一格的状态, 即如下图所示:

对于格子(3,4)(3,4)来说, 我们可以仅由这个格子推导到下一个状态.

b1b1为格子(3,4)(3,4)的左边界状态, b2b2为格子(3,4)(3,4)的上边界状态.

考虑如何表示状态.

括号表示法

考虑用一个kk进制数表示状态.

轮廓线上从左到右再到上最后到右每一个位置分别存储在这个数的第0,1,,m0,1,\dots,m位.

00表示无插头, 11表示左端点, 22表示右端点.

举个例子:

我们用四进制数表示这个状态为: 202101202101, (图中从左往右为状态中低位到高位).

使用四进制而不使用三进制的原因: 使用四(2k)(2^k)进制可以利用位移加速运算.

右端点和左端点状态分别为1122, 而不是全都是11的原因:

b1=2,b2=1b1=2 ,b2=1b1=1,b2=2b1=1,b2=2的状态会产生不同的结果, 见下文分类讨论.

1.1.当前点为障碍点.

此时只有b1=0,b2=0b1=0,b2=0才是合法的状态.

且转移后的状态与转移前的状态相同.

2.2.当前点不是障碍点

(1).b1=0,b2=0(1).b1=0,b2=0.

为确保每个点都被走过, 加入一个右插头和一个下插头.

(2).b1=0,b20(2).b1=0,b2\neq0.

b2b2所表示的插头可以选择直走, 或者选择向右拐, 括号状态不变.

(3).b10,b2=0(3).b1\neq0,b2=0.

情况同(2)(2), 即b1b1所表示的插头可以选择直走, 或者选择向下拐, 括号状态不变.

(4).b1=1,b2=1(4).b1=1,b2=1.

两个左端点对接, 此时b2b2所对应的右端应变为新状态的左端(否则括号数量不匹配).

做法: O(m)O(m)向右扫描插头, 当右端点比左端点数量大11时, 则这个右端点才可能是b2b2的另一端.

将这个右端点修改为左端点即可.

(5).b1=2,b2=2(5).b1=2,b2=2.

情况同(4)(4), 两个右端点对接, 此时b1b1所对应的左端应变为新状态的右端(否则括号数量不匹配).

做法: O(m)O(m)向左扫描插头, 当左端点比右端点数量大11时, 则这个左端点才可能是b1b1的另一端.

将这个左端点修改为右端点即可.

(6).b1=2,b2=1(6).b1=2,b2=1.

此时两个不同情况的插头对接.

不同于情况(4)(4)和情况(5)(5), 我们可以直接令这两个插头对接, 所对应的另外两个插头仍然是匹配的.

(7).b1=1,b2=2(7).b1=1,b2=2.

此时说明这个回路闭合.

若这个点为终点(最后一个合法的格子), 更新答案.

否则情况不合法, 不计入答案.

88种情况可以表示出所有的状态.

时间复杂度:O(nm4m+1)O(nm*4^{m+1}).

我们通过合法状态进行转移.

由于合法状态极少, 所以常数极小, 时间上可以通过.

空间复杂度:O(nm4m+1)O(nm*4^{m+1}).

考虑优化, 当前状态只会受到上一个格子状态的影响.

使用滚动数组进行优化.

空间复杂度:O(24m+1)=O(22m+3)O(2*4^{m+1})=O(2^{2m+3}).

然而还是不行, 然后这题不可做.

仍然是因为合法状态少, 因此我们将状态HashHash处理.

HashHash采用挂表法实现即可.

空间复杂度与合法状态数有关.

因此, 空间复杂度:O(O(能过)).

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int N = 15, hs = 299987;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
char ch[N];
int n, m, s, t, now, last, ans;
int a[N][N], bit[N];
int head[300000], nxt[2<<24], cnt[2];
struct node { int sta, val; } e[2][2<<24];
void ins(int sta, int num) {
	int key = sta % hs + 1;
	for(int i = head[key]; i; i = nxt[i]) {
		if(e[now][i].sta == sta) return e[now][i].val += num, (void)0;
	}
	e[now][++ cnt[now]] = (node) {sta, num};	//挂表;
	nxt[cnt[now]] = head[key];
	head[key] = cnt[now];
}
signed main() {
	n = read(); m = read();
	for(int i = 1; i <= n; ++ i) {
		cin >> (ch + 1);
		for(int j = 1; j <= m; ++ j) {
			if(ch[j] != '.') continue;
			a[i][j] = 1; s = i; t = j;	//s,t记录最后一个合法格子的位置;
		}
	}
	bit[0] = 1;
	last = 0; now = 1;
	for(int i = 1; i <= 13; ++ i) {	//预处理四进制每一位的权值;
		bit[i] = bit[i - 1] << 2;
	}
	ins(0, 1);	//初始化, 没有回路算一种方案;
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= cnt[now]; ++ j) {	//行间转移;
			e[now][j].sta <<= 2;
		}
		for(int j = 1; j <= m; ++ j) {
			memset(head, 0, sizeof(head));	//清空哈希表;
            swap(last, now); cnt[now] = 0;	//滚动数组;
			for(int k = 1; k <= cnt[last]; ++ k) {	//枚举上一个格子和合法状态;
				int sta = e[last][k].sta, num = e[last][k].val;
				int b1 = (sta >> ((j - 1) * 2)) % 4;	//四进制提取状态;
				int b2 = (sta >> (j * 2)) % 4;
				if(a[i][j] == 0) {	//大力讨论...
					if(b1 == 0 && b2 == 0) ins(sta, num);
				}
				else if(b1 == 0 && b2 == 0) {
					if(a[i+1][j] && a[i][j+1]) ins(sta + bit[j-1] + bit[j] * 2, num);
				}
				else if(b1 == 0 && b2) {
					if(a[i][j + 1]) ins(sta, num);
					if(a[i + 1][j]) ins(sta - bit[j] * b2 + bit[j-1] * b2, num);
				}
				else if(b1 && b2 == 0) {
					if(a[i+1][j]) ins(sta, num);
					if(a[i][j + 1]) ins(sta - bit[j-1]*b1 + bit[j] * b1, num);
				}
				else if(b1 == 1 && b2 == 1) {
					int k1 = 1;
					for(int l = j + 1; l <= m; ++ l) {
						if((sta >> (l * 2)) % 4 == 1) k1 ++;
						if((sta >> (l * 2)) % 4 == 2) k1 --;
						if(k1 == 0) {
							ins(sta - bit[j] - bit[j-1] - bit[l], num);
							break;
						}
					}
				}
				else if(b1 == 2 && b2 == 2) {
					int k1 = 1;
					for(int l = j - 2; ~l; -- l) {
						if((sta >> (l * 2)) % 4 == 1) k1 --;
						if((sta >> (l * 2)) % 4 == 2) k1 ++;
						if(k1 == 0) {
							ins(sta - bit[j] * 2 - bit[j-1] * 2 + bit[l], num);
							break;
						}
					}
				}
				else if(b1 == 2 && b2 == 1) ins(sta - bit[j-1]*2 - bit[j], num);
				if(i == s && j == t) ans += num;
			}
		}
	}
	printf("%lld\n", ans);
	return 0;
}

P5074 Eat the Trees

题目大意:

给出nmn*m的方格,有些格子不能铺线,其它格子必须铺,形成多个闭合回路。

问有多少种铺法?

2n,m122\leq n,m\leq 12.

题解:

此题中可以形成多个回路.

在上一题中, 我们使用括号表示法区分左右端点的原因是合并的端点不同时, 对状态转移的影响不同.

实际上是为了防止回路提前闭合而形成多个回路, 形成不合法状态.

而在这道题中, 虽然左右端点对方案总数有影响, 但对方案数的转移无影响.

因此我们不必区分左右端点.

使用二进制来表示即可.00表示无插头, 11表示有插头.

分情况转移方案数即可.

特别的, 当整张图全部为00时, 也算一种合法方案.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int N = 15, hs = 299987;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int T, n, m, now, last, s, t, a[N][N], bit[20], vis[N];
int cnt[2], head[300000], nxt[2<<24];
struct node { int sta, val; } e[2][2<<24];
void ins(int sta, int num) {
	int key = sta % hs + 1;
	for(int i = head[key]; i; i = nxt[i]) {
		if(e[now][i].sta == sta) return e[now][i].val += num, (void)0;
	}
	e[now][++ cnt[now]] = (node) {sta, num};
	nxt[cnt[now]] = head[key];
	head[key] = cnt[now];
}
void solve() {
	memset(cnt, 0, sizeof(cnt));
	memset(vis, 0, sizeof(vis));
	memset(head, 0, sizeof(head));
	int ans = 0;
	last = 0; now = 1;
	ins(0, 1);
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= cnt[now]; ++ j) {
			e[now][j].sta <<= 1;
		}
		for(int j = 1; j <= m; ++ j) {
			swap(now, last);
			memset(head, 0, sizeof(head));
			cnt[now] = 0;
			for(int k = 1; k <= cnt[last]; ++ k) {
				int sta = e[last][k].sta, num = e[last][k].val;
				int b1 = (sta >> (j-1)) & 1;
				int b2 = (sta >> j) & 1;
				if(a[i][j] == 0) {
					if(b1 == 0 && b2 == 0) ins(sta, num);
				}
				else if(b1 == 0 && b2 == 0) {
					if(a[i][j + 1] && a[i + 1][j]) ins(sta + bit[j-1] + bit[j], num);
				}
				else if(b1 == 0 && b2) {
					if(a[i][j + 1]) ins(sta, num);
					if(a[i + 1][j]) ins(sta + bit[j-1] - bit[j], num);
				}
				else if(b1 && b2 == 0) {
					if(a[i][j + 1]) ins(sta - bit[j-1] + bit[j], num);
					if(a[i + 1][j]) ins(sta, num);
				}
				else {
					if(i == s && j == t) ans += num;
					else ins(sta - bit[j-1] - bit[j], num);
				}
			}
		}
	}
	printf("%lld\n", ans);
}
signed main() {
	T = read();
	bit[0] = 1;
	for(int i = 1; i <= 13; ++ i) {
		bit[i] = bit[i - 1] << 1;
	}
	while(T --> 0) {
		n = read(); m = read();
		memset(a, 0, sizeof(a));
		s = t = 0;
		for(int i = 1; i <= n; ++ i) {
			for(int j = 1; j <= m; ++ j)  {
				if((a[i][j] = read()) == 1) { s = i; t = j; }
			}
		}
		if(s == 0 && t == 0) puts("1");
		else solve();
	}
	return 0;
}

P3886 [JLOI2009]神秘的生物

题目大意:

nn(n9)n*n(n\leq9)的带权网格, 求某个联通块的最大和.

题解:

插头DPDP. 最小表示法.

对于每个格子, 只有两种状态: 选 oror 不选.

重定义轮廓线如下图所示:

88进制数表示轮廓线的状态.

表示方法: 最小表示法.

假设有一个序列(5,5,3,2,4,1,3,2)(5,5,3,2,4,1,3,2), 序列中的每一个数代表第ii个格子所属的联通分量.

可以将编号等效映射成其他编号.

为了连通性表示的规范和不重不漏, 我们将这个序列整理成意义等效的前提下字典序最小的形式.

即此序列的最小表示为(1,1,2,3,4,5,2,3)(1,1,2,3,4,5,2,3).

方法:

从前向后遍历, 若当前编号为第一次出现, 将其换为新编号tot+1tot+1.

否则将其改为已经记录下来的对应编号.

void Min_express(int num) {
	int tot = 0;
	for(int i = 1; i <= n; ++ i) {
		if(s[i]) {
			if(bel[s[i]]) s[i] = bel[s[i]];
			else s[i] = bel[s[i]] = ++ tot;
		}
	}
}

考虑状态转移.

我们不能仅仅考虑每个格子上面的格子和左面的格子.

否则无法处理如下情况:

对于红色格子来说, 如果只考虑其上面和左面的格子, 就无法考虑到这种连通情况.

考虑转移:

直接考虑选不选这个格子, 不选它就判断它上面的格子会不会失去连通性.

如果选择, 看一下是合并连通块还是新建连通块.

对于每个格子有两个状态.

  • 不选

为考虑状态的合理性, 且最后要保证只有一个连通块.
斯坦纳树

我们只考虑上面的格子, 对于左面的格子, 拖到下一行考虑.

(1).b1=0,b2=0(1).b1=0,b2=0.

这个格子单独形成一个新块, 将此块命名为77.

命名为77的原因:

在最小表示法下, 连通块的标号从小到大记录, 为了防止和已有的标号冲突.

将新块的标号尽量调大, 到后面再用最小表示法整理状态序列.

(2).b1=1b2=1(2).b1=1||b2=1.

这一个格子被选择后, 会与其他已存在的格子连通, 更新状态即可.

特别的, 一个格子都不选时, 价值为00.

每次更新状态O(n)O(n)标号.

时间复杂度:O(n+12nn3)O\Bigg(\left \lfloor \displaystyle \frac{n+1}{2}\right \rfloor ^nn^3 \Bigg).无用状态过多.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int N = 15, hs = 299987;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int n, ans = -2e9, bit[N], a[N][N], head[300000], cnt[2], s[N], nxt[1<<24];
int now = 0, last = 1;
struct node { int sta, val; } e[2][1<<24];
void ins(int sta, int num) {
	int key = sta % hs + 1;
	for(int i = head[key]; i; i = nxt[i]) {
		if(e[now][i].sta == sta) {
			return e[now][i].val = Max(e[now][i].val, num), (void)0;
		}
	}
	e[now][++ cnt[now]] = (node) {sta, num};
	nxt[cnt[now]] = head[key];
	head[key] = cnt[now];
}
int upd(int num) {	//将s[]数组利用最小表示法表示后, 返回一个8进制状态数, 若状态合理则更新答案;
	int bel[N];
	memset(bel, 0, sizeof(bel));
	int tot = 0;
	for(int i = 1; i <= n; ++ i) {
		if(s[i]) {
			if(bel[s[i]]) s[i] = bel[s[i]];
			else s[i] = bel[s[i]] = ++ tot;
		}
	}
	int res = 0;
	for(int i = 1; i <= n; ++ i) {
		res += (s[i] << bit[i - 1]);
	}
	if(tot == 1) ans = Max(ans, num);
	return res;
}
void get_bitset(int sta) {	//将状态存储到s[]数组中;
	for(int i = 1; i <= n; ++ i) {
		s[i] = (sta >> bit[i - 1]) % 8;
	}
}
signed main() {
	n = read();
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= n; ++ j) {
			a[i][j] = read();
		}
	}
	for(int i = 1; i <= 10; ++ i) {
		bit[i] = i * 3;
	}
	ins(0, 0);	//无连通块时价值为0;
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= n; ++ j) {
			swap(now, last); cnt[now] = 0;
			memset(head, 0, sizeof(head));
			for(int k = 1; k <= cnt[last]; ++ k) {
				get_bitset(e[last][k].sta);	//得到上一个格子的状态, 考虑不选的情况;
				int b1 = s[j - 1], b2 = s[j];
				int num = e[last][k].val;
				int sum = 0; s[j] = 0;
                //↓下↓sum记录轮廓线上和b2属于同一个连通块的格子数量(不包括b2);
				for(int l = 1; l <= n; ++ l) {	
					if(s[l] == b2) sum ++;
				}
				if(b2 == 0 || sum) ins(upd(num), num);
				num += a[i][j];	//即选择了这个格子;
				get_bitset(e[last][k].sta);	//得到上一个格子的状态, 考虑选的情况;
				if(b1 == 0 && b2 == 0) s[j] = 7;	//形成一个新连通块;
				else {
					s[j] = Max(b1, b2);	//连通;
					for(int l = 1; l <= n; ++ l) {	//表示成相同的数;
						if(s[l] && s[l] == Min(b1, b2)) {
							s[l] = Max(b1, b2);
						}
					}
				}
				ins(upd(num), num);
			}
		}
	}
	printf("%lld\n", ans);
	return 0;
}

P3190 [HNOI2007]神奇游乐园

题目大意:

nm(2n100,2m6)n*m(2\leq n\leq100,2\leq m\leq 6)的带权网格, 求一个最大权闭合回路.

题解:

大力插头DP​即可.

由于是一个闭合回路, 因此采用括号表示法.

分类讨论:

(1).b1=0,b2=0(1).b1=0,b2=0.

可以不连接插头, 也可以加入一个右插头和下插头.

(2).b1=0,b20(2).b1=0,b2\neq0.

b2b2所表示的插头可以选择向下走, 或者选择向右拐, 括号状态不变.


(3).b10,b2=0(3).b1\neq0,b2=0.

情况同(22), 即b1b1所表示的插头可以选择向右走, 或者选择向下拐, 括号状态不变.


(4).b1=1,b2=1(4).b1=1,b2=1.

两个左端点对接, 此时b2b2所对应的右端应变为新状态的左端(否则括号数量不匹配).

做法: O(m)O(m)向右扫描插头, 当右端点比左端点数量大11时, 则这个右端点才可能是b2b2的另一端.

将这个右端点修改为左端点即可.


(5).b1=2,b2=2(5).b1=2,b2=2.

情况同(4)(4), 两个右端点对接, 此时b1b1所对应的左端应变为新状态的右端(否则括号数量不匹配).

做法: O(m)O(m)向左扫描插头, 当左端点比右端点数量大11时, 则这个左端点才可能是b1b1的另一端.

将这个左端点修改为右端点即可.


(6).b1=2,b2=1(6).b1=2,b2=1.

此时两个不同情况的插头对接.

不同于情况(44)和情况(55), 我们可以直接令这两个插头对接, 所对应的另外两个插头仍然是匹配的.


(7).b1=1,b2=2(7).b1=1,b2=2.

此时说明这个回路闭合.

若其他地方没有插头则更新答案.

否则情况不合法, 不计入答案.

下图分别表示合法状态和不合法状态.


滚动数组 + 哈希表 优化.

时间复杂度:O(nm4m+1)O(nm*4^{m+1}).

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int N = 15, hs = 299987;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
char ch[N];
int n, m, s, t, now, last, ans = -2e9;
int a[105][105], bit[N];
int head[300000], nxt[2<<24], cnt[2];
struct node { int sta, val; } e[2][2<<24];
int cur, pre;
void ins(int sta, int val) {
	int key = sta % hs + 1;
	for(int i = head[key]; i; i = nxt[i]) {
		if(e[cur][i].sta == sta) {
			e[cur][i].val = Max(e[cur][i].val, val);
			return;
		}
	}
	e[cur][++ cnt[cur]] = (node) {sta, val};
	nxt[cnt[cur]] = head[key];
	head[key] = cnt[cur];
}
signed main() {
	n = read(); m = read();
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= m; ++ j) {
			a[i][j] = read();
		}
	}
	for(int i = 1; i <= 10; ++ i) {
		bit[i] = i << 1;
	}
	cur = 1, pre = 0;
	cnt[cur] = 1;
    for(int i = 1; i <= n; ++ i) {
        for(int j = 1; j <= cnt[cur]; ++ j) {
			e[cur][j].sta <<= 2;
        }
		for(int j = 1; j <= m; ++ j) {
			swap(cur, pre);
			cnt[cur] = 0; 
			memset(head, 0, sizeof(head));
            for(int k = 1; k <= cnt[pre]; ++ k) {
                int now = e[pre][k].sta, val = e[pre][k].val;
                int b1 = (now >> bit[j-1]) % 4; 
				int b2 = (now >> bit[j]) % 4;
                if(b2 == 0 && b1 == 0) {
                    ins(now, val);
                    if(j != m) ins(now + (1<<bit[j-1]) + ((1<<bit[j])<<1), val + a[i][j]);
                }
                if(b2 && b1 == 0) {
                    ins(now - b2*(1<<bit[j]) + b2*(1<<bit[j-1]), val + a[i][j]);
                    if(j != m) ins(now, val + a[i][j]);
                }
                if(b2 == 0 && b1) {
                    ins(now, val + a[i][j]);
                    if(j != m) ins(now - b1*(1<<bit[j-1]) + b1*(1<<bit[j]), val + a[i][j]);
                }
                if(b2 == 1&&b1 == 1) {
                    int cnt = 1;
                    for(int l = j + 1; l <= m; ++ l) {
                        if((now >> bit[l]) % 4 == 1) cnt ++;
                        if((now >> bit[l]) % 4 == 2) cnt --;
                        if(cnt == 0) {
                            ins(now - (1<<bit[l]) - (1<<bit[j-1]) - (1<<bit[j]), val + a[i][j]);
                            break;
                        }
                    }
                }
                if(b2 == 2 && b1 == 2) {
                    int cnt = 1;
                    for(int l = j - 2; ~l; -- l) {
                        if((now >> bit[l]) % 4 == 1) cnt --;
                        if((now >> bit[l]) % 4 == 2) cnt ++;
                        if(cnt == 0) {
                            ins(now + (1<<bit[l]) - ((1<<bit[j-1])<<1) - ((1<<bit[j])<<1), val + a[i][j]);
                            break;
                        }
                    }
                } 
                if(b2 == 1 && b1 == 2) {
                    ins(now - ((1<<bit[j-1])<<1) - (1<<bit[j]), val + a[i][j]);
                }
				if(b2 == 2 && b1 == 1) {
                    if(now == ((1<<bit[j])<<1) + (1<<bit[j-1])) {
                        ans = Max(ans, val + a[i][j]);
					}
                }
            }
        }
    }
	printf("%lld\n", ans);
	return 0;
}

P3272 [SCOI2011]地板

题目大意:

给定一张RC(RC100)R*C(R*C\leq100)大小的网格图, 求用LL型地板铺满所有非障碍点的方案数.

题解:

插头DPDP.

发现对于存在的插头, 有两种状态: 拐过弯和未拐过弯.

因此我们这样定义插头: 00表示没有插头, 11表示未拐过弯的插头, 22表示拐过弯的插头.

分类讨论:

(1).b1=0,b2=0(1).b1=0,b2=0.

可以选择加入一个未拐弯的下插头 或 一个未拐弯的右插头 或 一个地板的拐弯处.

(2).b1=0,b2=1(2).b1=0,b2=1.

b2b2可以选择向下延伸直走, 或者 选择向右拐.

(3).b1=1,b2=0(3).b1=1,b2=0.

情况同(2)(2), b1b1可以选择向右延伸直走, 或者 选择向下拐.

(4).b1=0,b2=2(4).b1=0,b2=2.

b2b2可以选择向下延伸直走.

或者选择结束延伸, 去掉插头即可. 若此格子为终点, 可以更新答案.

(5).b1=2,b2=0(5).b1=2,b2=0.

情况同(4)(4),b1b1可以选择向右延伸直走.

或者选择结束延伸, 去掉插头即可. 若此格子为终点, 可以更新答案.

(6).b1=1,b2=1(6).b1=1,b2=1.

两个未拐过弯的插头形成一个拐过弯的插头.

滚动数组 + 哈希表 优化即可.

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int hs = 299987, mod = 20110520;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
char ch;
int n, m, s, t, ans, bit[25], a[105][105], b[105][105];
int cnt[2], cur, pre, head[300000], nxt[2<<20];
struct node { int sta, val; } e[2][2<<20];
void ins(int sta, int num) {
	int key = sta % hs + 1;
	for(int i = head[key]; i; i = nxt[i]) {
		if(e[cur][i].sta == sta) {
			(e[cur][i].val += num) %= mod;
			return;
		}
	}
	e[cur][++ cnt[cur]] = (node) {sta, num %  mod};
	nxt[cnt[cur]] = head[key];
	head[key] = cnt[cur];
}
signed main() {
	n = read(); m = read();
	for(int i = 1; i <= 20; ++ i) {	//预处理 i * 2;
		bit[i] = i << 1;
	}
	if(n > m) {	//(s,t)记录最后一个合法点位置;
		for(int i = 1; i <= n; ++ i) {
			for(int j = 1; j <= m; ++ j) {
				ch = getchar();
				while(ch != '*' && ch != '_') ch = getchar();
				a[i][j] = (ch == '_');
				if(a[i][j]) s = i, t = j;
			}
		}
	}
	else {	//将图向右躺下;
		for(int i = 1; i <= n; ++ i) {
			for(int j = 1; j <= m; ++ j) {
				ch = getchar();
				while(ch != '*' && ch != '_') ch = getchar();
				b[i][j] = (ch == '_');
			}
		}
		for(int i = 1; i <= m; ++ i) {
			for(int j = 1; j <= n; ++ j) {
				a[i][j] = b[j][i];
				if(a[i][j]) s = i, t = j;
			}
		}
		swap(n, m);
	}
	cur = 1;
	ins(0, 1);	//初始化;
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= cnt[cur]; ++ j) {	//行间转化;
			e[cur][j].sta <<= 2;
		}
		for(int j = 1; j <= m; ++ j) {
			swap(cur, pre); cnt[cur] = 0;	//滚动数组;
			memset(head, 0, sizeof(head));
			for(int k = 1; k <= cnt[pre]; ++ k) {
				int sta = e[pre][k].sta, num = e[pre][k].val;
				int b1 = (sta >> bit[j - 1]) % 4;	//取出状态;
				int b2 = (sta >> bit[j]) % 4;
				if(a[i][j] == 0) {
					if(b1 == 0 && b2 == 0) { ins(sta, num); continue; }
				}
				if(b1 == 0 && b2 == 0) {
					if(a[i+1][j] && a[i][j+1]) {	//拐弯处;
						ins(sta + ((1<<bit[j-1])<<1) + ((1<<bit[j])<<1), num);
					}
					if(a[i+1][j]) ins(sta + (1<<bit[j-1]), num);	//开始向下铺;
					if(a[i][j+1]) ins(sta + (1<<bit[j]), num);	//开始向右铺;
				}
				if(b1 == 1 && b2 == 0) {
					if(a[i][j+1]) ins(sta - (1<<bit[j-1]) + (1<<bit[j]),num);	//b1直走;
					if(a[i+1][j]) ins(sta + (1<<bit[j-1]), num);	//b1向下拐;
				}
				if(b2 == 1 && b1 == 0){
					if(a[i+1][j]) ins(sta - (1<<bit[j]) + (1<<bit[j-1]), num);	//b2直走;
					if(a[i][j+1]) ins(sta + (1<<bit[j]), num);	//b2右拐;
				}
				if(b1 == 2 && b2 == 0){
					if(i == s && j == t) (ans += num) %= mod;	
					if(a[i][j+1]) ins(sta - ((1<<bit[j-1])<<1) + ((1<<bit[j])<<1), num);
					ins(sta - ((1<<bit[j-1])<<1), num);
				}
				if(b2 == 2 && b1 == 0){
					if(i == s && j == t) (ans += num) %= mod;
					if(a[i+1][j]) ins(sta - ((1<<bit[j])<<1) + ((1<<bit[j-1])<<1), num);
                    ins(sta - ((1<<bit[j])<<1), num);	//切断;
				}
				if(b2 == 1 && b1 == 1) {
					if(i == s && j == t) (ans += num) %= mod;
					ins(sta - (1<<bit[j-1]) - (1<<bit[j]), num);	//合并;
				}
			}
		}
	}
	printf("%lld\n", ans);
	return 0;
}


P2289 [HNOI2004]邮递员

题目大意:

给定一张nmn*m的网格图, 求经过所有非障碍点一次的回路个数.

(1n20,1m10)(1\leq n\leq 20,1\leq m\leq 10)

题解:

双倍经验, 同P5056P5056【模板】插头dpdp

强化型模板题, 然而需要 高精 / __int128int128...

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 25, hs = 2501;
int read() {
	int x = 0, f = 1; char ch;
	while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
	for(x = ch^48; isdigit(ch = getchar()); x = (x<<3) + (x<<1) + (ch^48));
	return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
__int128 n, m, s, t, now, last, ans;
__int128 a[N][N], bit[N];
__int128 head[2505], nxt[2<<20], cnt[2];
struct node { __int128 sta, val; } e[2][2<<20];
inline void ins(__int128 sta, __int128 num) {
	__int128 key = sta % hs + 1;
	for(__int128 i = head[key]; i; i = nxt[i]) {
		if(e[now][i].sta == sta) return e[now][i].val += num, (void)0;
	}
	e[now][++ cnt[now]] = (node) {sta, num};
	nxt[cnt[now]] = head[key];
	head[key] = cnt[now];
}
inline void write(__int128 x) {
    if(x < 0) putchar('-'), x=-x;
    if(x > 9) write(x / 10);
    putchar(x % 10 + '0');
}
signed main() {
	m = read(); n = read();
	if(n == 1 || m == 1) return puts("1"), 0;
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= m; ++ j) {
			a[i][j] = 1;
		}
	}
	s = n, t = m;
	bit[0] = 1;
	last = 0; now = 1;
	for(int i = 1; i <= m; ++ i) {
		bit[i] = bit[i - 1] << 2;
	}
	ins(0, 1);
	for(int i = 1; i <= n; ++ i) {
		for(int j = 1; j <= cnt[now]; ++ j) {
			e[now][j].sta <<= 2;
		}
		for(int j = 1; j <= m; ++ j) {
			memset(head, 0, sizeof(head));
            swap(last, now);
			cnt[now] = 0;
			for(int k = 1; k <= cnt[last]; ++ k) {
				__int128 sta = e[last][k].sta, num = e[last][k].val;
				__int128 b1 = (sta >> ((j - 1) * 2)) % 4;
				__int128 b2 = (sta >> (j * 2)) % 4;
				if(a[i][j] == 0) {
					if(b1 == 0 && b2 == 0) ins(sta, num);
				}
				else if(b1 == 0 && b2 == 0) {
					if(a[i+1][j] && a[i][j+1]) ins(sta + bit[j-1] + bit[j] * 2, num);
				}
				else if(b1 == 0 && b2) {
					if(a[i][j + 1]) ins(sta, num);
					if(a[i + 1][j]) ins(sta - bit[j] * b2 + bit[j-1] * b2, num);
				}
				else if(b1 && b2 == 0) {
					if(a[i+1][j]) ins(sta, num);
					if(a[i][j + 1]) ins(sta - bit[j-1]*b1 + bit[j] * b1, num);
				}
				else if(b1 == 1 && b2 == 1) {
					__int128 k1 = 1;
					for(int l = j + 1; l <= m; ++ l) {
						if((sta >> (l * 2)) % 4 == 1) k1 ++;
						if((sta >> (l * 2)) % 4 == 2) k1 --;
						if(k1 == 0) {
							ins(sta - bit[j] - bit[j-1] - bit[l], num);
							break;
						}
					}
				}
				else if(b1 == 2 && b2 == 2) {
					__int128 k1 = 1;
					for(int l = j - 2; ~l; -- l) {
						if((sta >> (l * 2)) % 4 == 1) k1 --;
						if((sta >> (l * 2)) % 4 == 2) k1 ++;
						if(k1 == 0) {
							ins(sta - bit[j] * 2 - bit[j-1] * 2 + bit[l], num);
							break;
						}
					}
				}
				else if(b1 == 2 && b2 == 1) ins(sta - bit[j-1]*2 - bit[j], num);
				if(i == s && j == t) ans += num;
			}
		}
	}
	write(ans * 2);
	return 0;
}

参考文章:

【学习笔记】动态规划—各种 DP 优化 - 晨星凌
https://www.cnblogs.com/Xing-Ling/p/11317315.html

【学习笔记】动态规划—斜率优化DP(超详细)- 晨星凌
https://www.cnblogs.com/Xing-Ling/p/11210179.html

斜率优化学习笔记 - MashiroSky
https://www.cnblogs.com/MashiroSky/p/6009685.html

动态规划之四边形不等式优化 - ybwowen
https://www.cnblogs.com/ybwowen/p/11116654.html

斯坦纳树小结 - 自为风月马前卒
https://www.cnblogs.com/zwfymqz/p/8977295.html

虚树入门 - 自为风月马前卒
https://www.cnblogs.com/zwfymqz/p/9175152.html
洛谷日报[#185]浅谈虚树 - SSerxhs
https://sserxhs.blog.luogu.org/qian-tan-xu-shu

插头dp小结 - y2823774827y
https://www.cnblogs.com/y2823774827y/p/10140757.html

省选算法学习-插头dp - dedicatus545
https://www.cnblogs.com/dedicatus545/p/8638869.html

P3190 [HNOI2007]神奇游乐园
https://www.cnblogs.com/LLTYYC/p/11456786.htm

斜率优化dp总结 - _zjz
https://www.luogu.com.cn/blog/zjz-123-qaq/xie-shuai-you-hua-dp-zong-jie

P3886 [JLOI2009]神秘的生物 - GNAQ
https://www.luogu.com.cn/blog/GNAQ/solution-p3886

浅谈"fake树"——虚树 - 模拟退火

https://www.cnblogs.com/yexinqwq/p/10280773.html