可供参考学习博客: link
斜率单调,二元组横坐标单调
hdu 3507: link
很容易得到 dp 的表达式:
d
p
[
i
]
=
m
i
n
(
d
p
[
i
]
,
d
p
[
j
]
+
(
s
u
m
[
i
]
−
s
u
m
[
j
]
)
2
+
M
)
(
j
<
i
)
dp[i] = min(dp[i], \ dp[j] + (sum[i] - sum[j])^2 + M) \ (j < i)
dp[i]=min(dp[i], dp[j]+(sum[i]−sum[j])2+M) (j<i)
可以化成:
d
p
[
i
]
=
−
2
s
u
m
[
i
]
∗
s
u
m
[
j
]
+
d
p
[
j
]
+
s
u
m
2
[
j
]
+
s
u
m
2
[
i
]
+
M
dp[i] = -2sum[i] * sum[j] + dp[j] + sum^2[j] + sum^2[i] + M
dp[i]=−2sum[i]∗sum[j]+dp[j]+sum2[j]+sum2[i]+M,对于
i
i
i 来说,
s
u
m
[
i
]
,
M
sum[i], M
sum[i],M 是一个常数
若是把
(
s
u
m
[
j
]
,
d
p
[
j
]
+
s
u
m
2
[
j
]
)
(sum[j], \ dp[j] + sum^2[j])
(sum[j], dp[j]+sum2[j]) 放到一个坐标系中,即
x
=
s
u
m
[
j
]
,
y
=
d
p
[
j
]
+
s
u
m
2
[
j
]
,
c
=
s
u
m
2
[
i
]
+
M
x = sum[j], \ y = dp[j] + sum^2[j], \ c = sum^2[i] + M
x=sum[j], y=dp[j]+sum2[j], c=sum2[i]+M
可以得到 y = 2 s u m [ i ] x + d p [ i ] + c y = 2sum[i] \ x \ + \ dp[i] \ + \ c y=2sum[i] x + dp[i] + c,由线性规划知识,为使得纵截距越小,应该维护一个下凸包(斜率增大)。
由于斜率是单增的,所以可以用单调队列维护凸包,每次只要保证队列头的斜率大于 2 s u m [ i ] 2sum[i] 2sum[i];并且所加入的点横坐标单增,维护凸包只要对队尾操作即可。
#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 point
{
ll x, y;
point(ll k1, ll k2): x{k1}, y{k2} {}
point() {}
}q[maxn];
int n, m, tmp, L, R;
ll a[maxn], sum[maxn], dp[maxn];
void Pop(ll k) //维护一个下凸包,斜率单增
{
while(R >= L + 1 && (q[L+1].y - q[L].y) <= k * (q[L+1].x - q[L].x))
L++;
}
void Push(point p) //维护一个下凸包
{
ll x1, x2, y1, y2;
while(R >= L + 1)
{
x1 = p.x - q[R - 1].x;
y1 = p.y - q[R - 1].y;
x2 = p.x - q[R].x;
y2 = p.y - q[R].y;
if(x1 * y2 - x2 * y1 > 0)
break;
R--;
}
q[++R] = p;
}
int main()
{
while(~scanf("%d %d", &n, &m))
{
for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
for (int i = 1; i <= n; i++) sum[i] = sum[i-1] + a[i];
q[1].x = q[1].y = 0;
L = R = 1;
for (int i = 1; i <= n; i++)
{
Pop(2 * sum[i]);
dp[i] = -2 * sum[i] * q[L].x + q[L].y + sum[i] * sum[i] + m;
Push(point(sum[i], dp[i] + sum[i] * sum[i]));
}
printf("%lld\n", dp[n]);
}
}
P4360:link
首先可以算出只有山下锯木厂的费用,现在我们计算加了两个锯木厂之后节省了多少费用,则要求减少费用最大。
记
s
u
m
[
i
]
sum[i]
sum[i] 为
[
1
,
i
]
[1, i]
[1,i] 这些树的重量,
d
i
s
t
[
i
]
dist[i]
dist[i] 为第
i
i
i 棵树到达山下的距离。若
d
p
[
i
]
dp[i]
dp[i] 表示在
i
i
i 建立第二个锯木厂的最大节省费用(还有一个在
i
i
i 的上面),则有:
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
s
u
m
[
j
]
∗
d
i
s
t
[
j
]
+
(
s
u
m
[
i
]
−
d
i
s
t
[
j
]
)
∗
d
i
s
t
[
i
]
)
=
m
a
x
(
d
p
[
i
]
,
−
d
i
s
t
[
i
]
∗
s
u
m
[
j
]
+
s
u
m
[
j
]
∗
d
i
s
t
[
j
]
+
s
u
m
[
i
]
∗
d
i
s
t
[
i
]
)
dp[i] = max(dp[i], \ sum[j] * dist[j] + (sum[i] - dist[j]) * dist[i]) \ = max(dp[i], \ -dist[i] * sum[j] + sum[j] * dist[j] + sum[i] * dist[i])
dp[i]=max(dp[i], sum[j]∗dist[j]+(sum[i]−dist[j])∗dist[i]) =max(dp[i], −dist[i]∗sum[j]+sum[j]∗dist[j]+sum[i]∗dist[i])
若是把
(
s
u
m
[
j
]
,
d
i
s
t
[
j
]
∗
s
u
m
[
j
]
)
(sum[j], \ dist[j]*sum[j])
(sum[j], dist[j]∗sum[j]) 放到一个坐标系中,即
x
=
s
u
m
[
j
]
,
y
=
d
i
s
t
[
j
]
∗
s
u
m
[
j
]
,
c
=
s
u
m
[
i
]
∗
d
i
s
t
[
i
]
x = sum[j], \ y = dist[j]*sum[j], \ c = sum[i] * dist[i]
x=sum[j], y=dist[j]∗sum[j], c=sum[i]∗dist[i]
可以得到 y = d i s t [ i ] x + d p [ i ] − c y = dist[i] \ x \ + \ dp[i] \ - \ c y=dist[i] x + dp[i] − c,由线性规划知识,为使得纵截距越大,应该维护一个上凸包(斜率减小)。
由于斜率是单建的,所以可以用单调队列维护凸包,每次只要保证队列头的斜率小于 d i s t [ i ] dist[i] dist[i];并且所加入的点横坐标单增,维护凸包只要对队尾操作即可。
#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 = 5e4 + 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 point
{
ll x, y;
point(ll k1, ll k2): x{k1}, y{k2} {}
point() {}
}q[maxn];
int n, L, R;
ll d[maxn], sum[maxn], ans, res;
void Pop(ll k)
{
while(R >= L + 1 && (q[L+1].y - q[L].y) >= k * (q[L+1].x - q[L].x))
L++;
}
void Push(point p)
{
ll x1, x2, y1, y2;
while(R >= L + 1)
{
x1 = p.x - q[R - 1].x;
y1 = p.y - q[R - 1].y;
x2 = p.x - q[R].x;
y2 = p.y - q[R].y;
if(x1 * y2 - x2 * y1 < 0)
break;
R--;
}
q[++R] = p;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld %lld", &sum[i], &d[i]);
for (int i = 2; i <= n; i++) sum[i] += sum[i-1];
for (int i = 1; i <= n; i++) res += sum[i] * d[i];
for (int i = n - 1; i >= 1; i--) d[i] += d[i+1];
q[1].x = q[1].y = 0;
L = R = 1;
for (int i = 1; i <= n; i++)
{
Pop(d[i]);
ans = max(ans, sum[i] * d[i] - q[L].x * d[i] + q[L].y);
Push(point(sum[i], sum[i] * d[i]));
}
printf("%lld\n", res - ans);
}
斜率非单调,二元组横坐标非单调
这种条件的做法需要CDQ分治或者平衡树,摘自:link:
P4655: link
若设 d p [ i ] dp[i] dp[i] 表示处理完前 i i i 个柱子的最小费用, s u m [ i ] sum[i] sum[i] 表示前 i i i 个柱子的拆除费用。
转移为 d p [ i ] = m i n ( d p [ i ] , d p [ j ] + s u m [ i − 1 ] − s u m [ j ] + ( h [ i ] − h [ j ] ) 2 ) = − 2 h [ i ] ∗ h [ j ] + ( d p [ j ] − s u m [ j ] + h 2 [ j ] ) + s u m [ i − 1 ] + h 2 [ i ] dp[i] = min(dp[i], \ dp[j] + sum[i-1] - sum[j] + (h[i] - h[j])^2) = -2h[i] * h[j] + (dp[j] - sum[j] + h^2[j]) + sum[i-1] + h^2[i] dp[i]=min(dp[i], dp[j]+sum[i−1]−sum[j]+(h[i]−h[j])2)=−2h[i]∗h[j]+(dp[j]−sum[j]+h2[j])+sum[i−1]+h2[i]
若是把 ( h [ j ] , d p [ j ] − s u m [ j ] + h 2 [ j ] ) (h[j], \ dp[j] - sum[j] + h^2[j]) (h[j], dp[j]−sum[j]+h2[j]) 放到一个坐标系中,即 x = h [ j ] , y = d p [ j ] − s u m [ j ] + h 2 [ j ] , c = s u m [ i − 1 ] + h 2 [ i ] x =h[j], \ y = dp[j] - sum[j] + h^2[j], \ c = sum[i-1] + h^2[i] x=h[j], y=dp[j]−sum[j]+h2[j], c=sum[i−1]+h2[i]
可以得到 y = 2 h [ i ] x + d p [ i ] − c y = 2h[i] \ x \ + \ dp[i] \ - \ c y=2h[i] x + dp[i] − c,由线性规划知识,为使得纵截距越小,应该维护一个下凸包(斜率增大)。
但是这个斜率 2 h [ i ] 2h[i] 2h[i] 不单调,横坐标也不单调,所以需要 CDQ 分治来维护,具体细节可以见代码:
#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 = 1e5 + 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 point //来存决策点
{
ll x, y;
point(ll k1, ll k2): x{k1}, y{k2} {}
point() {}
bool operator<(const point &m)
{
return x ^ m.x ? x < m.x : y < m.y;
}
}a[maxn], q[maxn];
struct node //来存斜率
{
int id;
ll slope;
bool operator<(const node &m)
{
return slope < m.slope;
}
}b[maxn], tmpb[maxn];
int n;
ll h[maxn], dp[maxn], sum[maxn];
ll getx(int x)
{
return h[x];
}
ll gety(int x)
{
return dp[x] - sum[x] + h[x] * h[x];
}
void CDQ(int l, int r)
{
if(l == r) //当这个结点dp值已经被更新完之后,则它可以作为一个决策点,加入a数组
{
a[l].x = getx(l);
a[r].y = gety(r);
return;
}
//根据[L, mid]决策点来建立凸壳
int mid = (l + r) / 2, L = 1, R = 0;
ll x1, y1, x2, y2;
CDQ(l, mid);
sort(a + l, a + 1 + mid); //将决策点按照横坐标排序(下凸包横坐标相同时,取纵坐标小的)
q[++R] = a[l];
for (int i = l; i <= mid; i++)
{
if(a[i].x == q[L].x) continue;
while(L < R)
{
x1 = a[i].x - q[R - 1].x; y1 = a[i].y - q[R - 1].y;
x2 = a[i].x - q[R].x; y2 = a[i].y - q[R].y;
if(x1 * y2 - x2 * y1 > 0) //维护凸壳
break;
R--;
}
q[++R] = a[i];
}
//用左半部分的决策点更新[mid + 1, r]的dp值
for (int i = mid + 1; i <= r; i++) tmpb[i] = b[i];
sort(tmpb + 1 + mid, tmpb + 1 + r); //根据斜率排序
for (int i = mid + 1; i <= r; i++)
{
while(L < R && (q[L+1].y - q[L].y) <= tmpb[i].slope * (q[L+1].x - q[L].x))
L++;
dp[tmpb[i].id] = min(dp[tmpb[i].id], - tmpb[i].slope * q[L].x + q[L].y + sum[tmpb[i].id - 1] + h[tmpb[i].id] * h[tmpb[i].id]);
}
CDQ(mid + 1, r);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", &h[i]);
for (int i = 1; i <= n; i++) scanf("%lld", &sum[i]);
for (int i = 1; i <= n; i++) sum[i] += sum[i-1];
for (int i = 2; i <= n; i++) dp[i] = dp[i-1] + (h[i] - h[i-1]) * (h[i] - h[i-1]);
for (int i = 1; i <= n; i++) b[i].id = i, b[i].slope = 2 * h[i]; //提前处理好每个点的斜率
CDQ(1, n);
printf("%lld\n", dp[n]);
}