四边形不等式
对于这样的转移方程:
f
(
i
)
=
m
i
n
{
g
(
j
)
+
w
(
i
,
j
)
}
(
0
<
i
<
x
)
f(i) = min \{g(j) + w(i, j)\} \ (0 < i < x)
f(i)=min{g(j)+w(i,j)} (0<i<x),直接转移的话复杂度是
O
(
n
2
)
O(n^2)
O(n2) 的。但是如果满足四边形不等式,就可以用决策单调性就行优化。
证明:
假设
i
i
i 的决策点是
p
[
i
]
p[i]
p[i],那么对于
∀
j
∈
(
0
,
p
[
i
]
)
∀j ∈ (0, p[i])
∀j∈(0,p[i]),有
g
[
p
[
i
]
]
+
w
(
i
,
p
[
i
]
)
≤
g
[
j
]
+
w
(
i
,
j
)
①
g[p[i]] + w(i, p[i]) ≤ g[j] + w(i, j) \ \ ①
g[p[i]]+w(i,p[i])≤g[j]+w(i,j) ①
对于
∀
i
′
∈
(
i
,
n
]
∀i' ∈ (i, n]
∀i′∈(i,n],由于
j
<
p
[
i
]
<
i
<
i
′
j< p[i]<i <i'
j<p[i]<i<i′,有
w
(
i
,
j
)
+
w
(
i
′
,
p
[
i
]
)
≤
w
(
i
′
,
j
)
+
w
(
i
,
p
[
i
]
)
②
w(i, j) + w(i', p[i]) ≤ w(i', j) + w(i, p[i]) \ \ ②
w(i,j)+w(i′,p[i])≤w(i′,j)+w(i,p[i]) ②
对于②式即可以得到
w
(
i
′
,
p
[
i
]
)
−
w
(
i
,
p
[
i
]
)
≤
w
(
i
′
,
j
)
−
w
(
i
,
j
)
w(i', p[i]) - w(i, p[i]) ≤ w(i', j) - w(i, j)
w(i′,p[i])−w(i,p[i])≤w(i′,j)−w(i,j)
① + ③ 即可得到
g
[
p
[
i
]
]
+
w
(
i
′
,
p
[
i
]
)
≤
g
[
j
]
+
w
(
i
′
,
j
)
g[p[i]] + w(i', p[i]) ≤ g[j] + w(i', j)
g[p[i]]+w(i′,p[i])≤g[j]+w(i′,j),满足决策单调性
同理,如果是
f
(
i
)
=
m
a
x
{
g
(
j
)
+
w
(
i
,
j
)
}
(
0
<
i
<
x
)
f(i) = max \{g(j) + w(i, j)\} \ (0 < i < x)
f(i)=max{g(j)+w(i,j)} (0<i<x), 需要满足
a
<
b
<
c
<
d
,
w
(
d
,
a
)
+
w
(
c
,
b
)
≤
w
(
c
,
a
)
+
w
(
d
,
b
)
a < b < c<d, \ \ w(d, a) + w(c,b) ≤ w(c, a) + w(d, b)
a<b<c<d, w(d,a)+w(c,b)≤w(c,a)+w(d,b)
例题 link,让你求
p
i
=
m
a
x
(
a
j
+
∣
i
−
j
∣
)
(
1
≤
j
≤
n
)
p_i = max(a_j + \sqrt{|i - j|}) \ \ (1 ≤ j ≤ n)
pi=max(aj+∣i−j∣) (1≤j≤n)
经过化简,
p
i
=
m
a
x
(
m
a
x
1
≤
j
≤
i
(
a
j
+
i
−
j
)
,
m
a
x
i
≤
j
≤
n
(
a
j
+
j
−
i
)
)
p_i = max( max_{1 ≤ j ≤ i}(a_j + \sqrt{i - j}), max_{i ≤ j ≤ n}(a_j + \sqrt{j - i}))
pi=max(max1≤j≤i(aj+i−j),maxi≤j≤n(aj+j−i)),后半部分翻转一下就行了,所以问题划归成
p
i
=
m
a
x
1
≤
j
≤
i
(
a
j
+
∣
i
−
j
∣
)
(
1
≤
j
≤
n
)
p_i = max_{1 ≤ j ≤ i}(a_j + \sqrt{|i - j|}) \ \ (1 ≤ j ≤ n)
pi=max1≤j≤i(aj+∣i−j∣) (1≤j≤n)
对于
i
>
j
i > j
i>j,若记
w
[
i
,
j
]
=
i
−
j
w[i, j] = \sqrt{i-j}
w[i,j]=i−j,很容易可以证明
w
(
i
,
j
)
+
w
(
i
+
1
,
j
+
1
)
≥
w
(
i
,
j
+
1
)
,
w
(
i
+
1
,
j
)
w(i, j) + w(i + 1, j + 1) ≥ w(i, j + 1), w(i + 1, j)
w(i,j)+w(i+1,j+1)≥w(i,j+1),w(i+1,j)
经过扩展可以得到
a
<
b
<
c
<
d
,
w
(
d
,
a
)
+
w
(
c
,
b
)
≤
w
(
c
,
a
)
+
w
(
d
,
b
)
a < b < c<d, \ \ w(d, a) + w(c,b) ≤ w(c, a) + w(d, b)
a<b<c<d, w(d,a)+w(c,b)≤w(c,a)+w(d,b),满足四边形不等式
二分栈
在满足四边形不等式之后,就可以用决策单调性来优化,一种比较常见的手法是二分栈,每次得到了一个新的决策点,去更新当前这个决策点的最优区间(具体流程可以看上面那几个参考连接),可以做到
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 的复杂度。
但是二分栈也有它的劣势,就是我每得到一个新的决策点,我就能算出之后的 w 值,但是有些情况下是算不出来的,这时候需要分治。
这一题先用二分栈的模板解决。
#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 5e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;
struct node
{
int l, r, p;
} q[maxn];
int a[maxn], head, tail, n;
double dp[maxn], sq[maxn];
double W(int i, int j) //w[i, j],i > j,这里的 w 值直接看成整个转移式了
{
return a[j] + sq[i - j];
}
int Binary(int t,int x) { //对栈里的第 i 个三元组进行二分
int ans = q[t].r + 1, l = q[t].l, r = q[t].r;
while(l <= r)
{
int mid=(l + r) / 2;
if(W(mid, q[t].p) <= W(mid, x)) ans = mid,r = mid - 1; //求的是最大值
else l = mid + 1;
}
return ans;
}
void Insert(int i)
{
q[tail].l = max(q[tail].l, i); //更新边界
while(head <= tail && W(q[tail].l, i) >= W(q[tail].l, q[tail].p)) tail--; //求的是最大值
if(head > tail)
{
q[++tail].l = i;
q[tail].r = n;
q[tail].p = i;
}
else
{
int pos = Binary(tail, i);
if(pos > n) return;
q[tail].r = pos - 1;
q[++tail].l = pos;
q[tail].r = n;
q[tail].p = i;
}
}
void solve()
{
head = 1, tail = 0; //初始情况栈为空
for (int i = 1; i <= n; i++)
{
Insert(i); //插入第 i 个决策点
if(head <= tail && q[head].r < i) head++; //更新右指针
else q[head].l = i; //更新边界
dp[i] = max(dp[i], W(i, q[head].p)); //给第 i 个点做决策
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
sq[i] = sqrt(i);
}
solve();
for (int i = 1; i <= n / 2; i++) swap(a[i], a[n+1-i]), swap(dp[i], dp[n+1-i]);
solve();
for (int i = n; i >= 1; i--) printf("%d\n", (int)ceil(dp[i]) - a[i]);
}
分治
使用二分栈的时候有一个缺点,就是
w
(
i
,
j
)
w(i, j)
w(i,j) 需要很快的算出来,但是在有些情况下是不好算的。这个时候如果被决策点在将来不会成为决策点,变可以用分治的思想去维护。
例题:link 给定一个序列 a,要把它分成 k 个子段。每个子段的费用是其中相同元素的对数。求所有子段的费用之和的最小值。(k ≤ 20)
可以让
d
p
[
j
]
[
i
]
dp[j][i]
dp[j][i] 表示将前
i
i
i 个数分成了
j
j
j 段的最小费用,那么我们可以得到
d
p
[
j
]
[
i
]
=
d
p
[
j
−
1
]
[
k
]
+
w
(
k
+
1
,
i
)
dp[j][i] = dp[j-1][k] + w(k+1, i)
dp[j][i]=dp[j−1][k]+w(k+1,i),其中
w
(
k
+
1
,
i
)
w(k+1, i)
w(k+1,i) 表示
[
k
+
1
,
i
]
[k+1, i]
[k+1,i] 为一段的费用。
这个 dp 可以证出是符合决策单调性的,但是不能
O
(
1
)
O(1)
O(1) 的计算
w
w
w 值,所以这里可以用到分治。
分治的时候给定一段决策点区间和一段被决策点区间,然后计算被决策点mid 的值,计算方法是遍历决策点,所以需要
O
(
n
)
O(n)
O(n)解决。
在这题中
w
w
w 值如果每个拆开算,每个需要
O
(
n
)
O(n)
O(n),总复杂度就是
O
(
n
2
)
O(n^2)
O(n2),但是如果用莫队的思想进行维护的话, 复杂度是
O
(
n
)
O(n)
O(n)。具体细节在代码中。
#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 5e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;
ll dp[21][maxn], ans;
int cnt[maxn], a[maxn], k, L, R, cur, n;
ll cal(int l, int r) //计算w[l, r]
{
while(L < l) ans -= cnt[a[L]] - 1, cnt[a[L]]--, L++;
while(L > l) L--, ans += cnt[a[L]], cnt[a[L]]++;
while(R > r) ans -= cnt[a[R]] - 1, cnt[a[R]]--, R--;
while(R < r) R++, ans += cnt[a[R]], cnt[a[R]]++;
return ans;
}
void solve(int dl, int dr, int l, int r) //dl, dr为决策点,l,r为被决策的区间
{
if(dl > dr || l > r) return;
int mid = (l + r) / 2;
int p;
ll res = 1e18, tmp;
for (int i = dl; i <= dr; i++) //给mid暴力计算出w值
{
tmp = cal(i + 1, mid); //处理左儿子的时候只动L,左儿子到右儿子移动L,R,右儿子继续移动L
if(res > dp[cur-1][i] + tmp)
res = dp[cur-1][i] + tmp, p = i;
}
dp[cur][mid] = res;
solve(dl, p, l, mid - 1); //分治
solve(p, dr, mid + 1, r);
}
int main()
{
scanf("%d %d", &n, &k);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
cnt[a[1]]++; //先指定一个L,R
L = R = 1;
cur = 1;
solve(0, 0, 1, n);
for (cur = 2; cur <= k; cur++) solve(cur - 1, n, cur, n);
printf("%lld\n", dp[k][n]);
}
link
这题也是和上一题一个套路,几乎一样的转移式子,就稍微改一下就行,但是复杂度是
O
(
n
k
l
o
g
n
)
O(nklogn)
O(nklogn) 的,用快读才会刚刚卡过,更好的做法是 wqs 二分。(待补)
要注意的是,莫队移动指针的时候可能有 l > r的情况(即有人坐不上船),这时候记得直接返回0。
#include <bits/stdc++.h>
#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 4010;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
//const double pi = acos(-1.0);
const double eps = 1e-11;
int w[maxn][maxn], sum[maxn][maxn];
int dp[802][maxn], L, R, n, k, cur, ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int cal(int l, int r) //计算w[l, r]
{
if(l > r) return 0;
while(L < l) ans -= sum[L][R], L++;
while(L > l) L--, ans += sum[L][R];
while(R > r) ans -= sum[R][L], R--;
while(R < r) R++, ans += sum[R][L];
return ans;
}
void solve(int dl, int dr, int l, int r) //dl, dr为决策点,l,r为被决策的区间
{
if(dl > dr || l > r) return;
int mid = (l + r) / 2;
int p;
int res = INF, tmp;
for (int i = dl; i <= dr; i++) //给mid暴力计算出w值
{
tmp = cal(i + 1, mid); //处理左儿子的时候只动L,左儿子到右儿子移动L,R,右儿子继续移动L
if(res > dp[cur-1][i] + tmp)
res = dp[cur-1][i] + tmp, p = i;
}
dp[cur][mid] = res;
solve(dl, p, l, mid - 1); //分治
solve(p, dr, mid + 1, r);
}
int main()
{
n = read(); k = read();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
w[i][j] = read();
for (int i = 1; i <= n; i++)
{
for (int j = i + 1; j <= n; j++)
sum[i][j] = sum[i][j - 1] + w[i][j];
for (int j = i - 1; j >= 0; j--)
sum[i][j] = sum[i][j + 1] + w[i][j];
}
L = R = 1;
for (cur = 1; cur <= k; cur++)
{
if(cur == 1) solve(0, 0, cur, n);
else solve(cur - 1, n, cur, n);
}
printf("%d\n", dp[k][n]);
}