李超线段树
<p><strong>李超线段树模板及应用</strong></p><p>李超线段树用于一系列平面上的一次函数,维护对于每一个 <span class="math inline">\(\texttt{x}\)</span> 最大或最小的 <span class="math inline">\(\texttt{y}\)</span> 值。</p>
<h1 id="模板题">模板题</h1>
<p>这道模板题非常全面,相比应用李超线段树的时候实现的东西要多的多:</p>
<ul>
<li>一是给的是横纵坐标,所以斜率要用 <span class="math inline">\(\texttt{double}\)</span> 类型,整个题的就都要考虑精度问题。</li>
<li>二是输出的是线段编号,所以 <span class="math inline">\(\texttt{query}\)</span> 函数要把值和标号一起传,同时因为要求输出编号小的,比较的时候也要多比较一个参数。</li>
<li>三是给的是线段而不是直线,要写一个函数去判断哪些区间被线段完全覆盖。</li>
<li>四是数据强制在线,这个有点毒瘤了。</li>
</ul>
<p>第一次做李超线段树也可以先考虑做P4254,是一道更正常的模板题。</p>
<h2 id="做法">做法</h2>
<p>先考虑如何在插入一条新的线段的时候维护答案。</p>
<p>显然查询完全被线段覆盖的区间是用普通线段树区间查询的方法容易实现的。</p>
<pre><code>void modify(int x, int l, int r, int x0, int x1, int id){
if(r < x0 || l > x1) return;
if(l >= x0 && r <= x1) update(x, l, r, id);
else {
int mid = (l + r) >> 1;
modify(2 * x, l, mid, x0, x1, id);
modify(2 * x + 1, mid + 1, r, x0, x1, id);
}
}
</code></pre>
<p>然后是李超线段树的重点,当一条线段完全覆盖某个区间时,怎么用这条线段修改这个区间的答案。</p>
<p>设 <span class="math inline">\(\text{tg}\)</span> 为原本这个直线最大的直线,要用于修改的新直线为 <span class="math inline">\(\text{f}\)</span>。</p>
<p>首先一个最基本且显然的逻辑是如果对于整个区间,直线 <span class="math inline">\(\text{f}\)</span> 上的值都大于 <span class="math inline">\(\text{tg}\)</span> 的值,那么就可以直接把 <span class="math inline">\(\text{tg}\)</span> 修改为 <span class="math inline">\(\text{f}\)</span>。</p>
<p>那么怎么判断呢?</p>
<p>我们先比较在区间中点 <span class="math inline">\(\text{mid}\)</span>,如果就 <span class="math inline">\(\text{f}\)</span> 更大就 <span class="math inline">\(\text{swap}\)</span>。</p>
<p><span class="math inline">\(\text{swap}\)</span> 后会有以下三种情况:</p>
<ul>
<li>
<p>没有交点:由于保证了在中点处 <span class="math inline">\(\text{tg}\)</span> 更大,此时在整个区间内 <span class="math inline">\(\text{tg}\)</span> 一定都更大,不需要再修改,直接return即可。<br>
<img src="https://img2024.cnblogs.com/blog/3695173/202603/3695173-20260320151942545-1648835846.png"></p>
</li>
<li>
<p>在 <span class="math inline">\(\text{mid}\)</span> 左边有交点:递归修改左边即可。<br>
<img src="https://img2024.cnblogs.com/blog/3695173/202603/3695173-20260320153115264-1837332334.png"></p>
</li>
<li>
<p>在 <span class="math inline">\(\text{mid}\)</span> 右边更大:递归修改右边即可。<br>
<img src="https://img2024.cnblogs.com/blog/3695173/202603/3695173-20260320153642715-1971508552.png"></p>
</li>
</ul>
<p>怎么判断两条线段在两边有没有交点呢?因为我们知道 <span class="math inline">\(\text{f}\)</span> 在 <span class="math inline">\(\text{mid}\)</span> 处更小,所以我们可以比较两条线段在 <span class="math inline">\(\text{l}\)</span> 和 <span class="math inline">\(\text{r}\)</span> 处的大小,如果 <span class="math inline">\(\text{f}\)</span> 在 <span class="math inline">\(\text{l}\)</span> 处更大,那么两条线段一定在左边有交点, <span class="math inline">\(\text{r}\)</span> 同理。</p>
<p>因为两边至多有一边会被继续递归修改(否则 <span class="math inline">\(\text{f}\)</span> 肯定会在第一步被 swap),复杂度为<span class="math inline">\(O(logn)\)</span>。</p>
<pre><code>int cmp(double a, double b){
if(a - b > eps) return 1;
else if(b - a > eps) return -1;
else return 0;
}
void makeline(int x0, int y0, int x1, int y1){
tot++;
if(x0 == x1){
l.k = 0;
l.b = max(y0, y1);
}
else {
l.k = (double)(y1 - y0) / (x1 - x0);
l.b = y0 - l.k * x0;
}
}
double yz(int x, int id){
return l.k * x + l.b;
}
void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
int cp = cmp(yz(mid, id), yz(mid, v));
if((cp == 1) || (!cp && id < v)) swap(id, v);
int cp1 = cmp(yz(l, id), yz(l, v));
int cp2 = cmp(yz(r, id), yz(r, v));
if((cp1 == 1) || (!cp1 && id < v)) update(2 * x, l, mid, id);
if((cp2 == 1) || (!cp2 && id < v)) update(2 * x + 1, mid + 1, r, id);
}
</code></pre>
<p>最后的 <span class="math inline">\(\texttt{query}\)</span> 函数就很简单了,直接沿着有要查询的点 <span class="math inline">\(\texttt{k}\)</span> 的区间跳 <span class="math inline">\(logn\)</span> 次记录路径上的最大值即可。</p>
<pre><code>out query(int x, int l, int r, int k){
if(l > k || r < k) return out(0, 0);
double ans = yz(k, tg);
if(l == r) return out(ans, tg);
int mid = (l + r) >> 1;
return max(out(ans, tg), max(query(2 * x, l, mid, k), query(2 * x + 1, mid + 1, r, k)));
}
</code></pre>
<p>板子的代码看起来很长,但是实际上在应用时数据大多都是整数类型,而且都是直线而不是线段,李超线段树的代码是很简洁的,一般来说大概长这样:</p>
<pre><code>struct line{
int k, b;
line(int x = 0, int y = 0){
k = x, b = y;
}
};
struct lctree{
line l;
int tg;
inline int f(int x, int id){
return l.k * x + l.b;
}
inline void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(mid, id) < f(mid, v)) swap(v, id);
if(f(l, id) < f(l, v)) update(2 * x, l, mid, id);
if(f(r, id) < f(r, v)) update(2 * x + 1, mid + 1, r, id);
}
inline pi query(int x, int l, int r, int k){
pi out;
out.first = f(k, tg);
out.second = tg;
if(l == r) return out;
int mid = (l + r) >> 1;
if(k <= mid) return min(out, query(2 * x, l, mid, k));
else return min(out, query(2 * x + 1, mid + 1, r, k));
}
}
</code></pre>
<p>奉上板子题完整代码:<br>
AC记录</p>
<h1 id="在斜率优化上的应用">在斜率优化上的应用</h1>
<p>李超线段树常用于斜率优化,即将dp式子中需要动态维护的一个区间最值的式子化成一个一次函数,这样就可以用李超线段树来维护,<span class="math inline">\(O(logn)\)</span> 的时间就能找到最值,从而优化时间复杂度。</p>
<p>以下题解按题号排序而非更新时间,所以讲解并非由详细到简略。</p>
<h2 id="p3195">P3195</h2>
<p>这道题式子化简还是相当麻烦的。</p>
<p>设长度 <span class="math inline">\(c_i\)</span> 的前缀和为 <span class="math inline">\(s_i\)</span>,那么长度 <span class="math inline">\(x = i - j - 1 + s_i - s_j\)</span>,代入 <span class="math inline">\(x\)</span>,得到:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + (i - j + s_i - s_j - L - 1)^2 \}
\]</div><p></p><p>设 <span class="math inline">\(t_i = s_i + i\)</span>,<span class="math inline">\(b = L + 1\)</span>,那么:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + (t_i - t_j - b)^2 \}
\]</div><p></p><p>展开平方:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{t_i^2 - 2(t_j + b)t_i + (t_j + b)^2 \}
\]</div><p></p><p>提出 <span class="math inline">\(t_i^2\)</span>:</p>
<p></p><div class="math display">\[\large dp_i = t_i^2 + \min_{j<i}\{-2(t_j + b)t_i + (t_j + b)^2 \}
\]</div><p></p><p>观察到 <span class="math inline">\(\texttt{min}\)</span> 内为一次函数,用李超线段树维护,每次查询 <span class="math inline">\(x=t_i\)</span> 的最小值。</p>
<p>另外注意到前缀和的值域较大,所以需要离散化或动态开点。</p>
<h3 id="代码">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e4 + 5;
int n, L, c, len, tot, sum, t, a, dp, tg;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x; b = y;
}
}l;
int g(int x){
return lower_bound(a + 1, a + len + 1, x) - a;
}
int f(int x, int id){
return l.k * x + l.b;
}
void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(a, id) < f(a, v)) swap(v, id);
if(f(a, id) < f(a, v)) update(2 * x, l, mid, id);
if(f(a, id) < f(a, v)) update(2 * x + 1, mid + 1, r, id);
}
int query(int x, int l, int r, int k){
if(l > k || r < k) return 1e18;
int ans = f(a, tg);
if(l == r) return ans;
int mid = (l + r) >> 1;
return min(ans, min(query(2 * x, l, mid, k), query(2 * x + 1, mid + 1, r, k)));
}
signed main(){
cin >> n >> L;
for(int i = 1; i <= n; i++){
cin >> c;
sum = sum + c;
t = sum + i;
a = t;
}
sort(a + 1, a + n + 1);
len = unique(a + 1, a + n + 1) - a - 1;
L++;
l.k = -2 * L;
l.b = L * L;
dp = (c - L + 1) * (c - L + 1);
l[++tot] = line(-2 * t, t * t + 2 * t * L + dp);
update(1, 1, len, tot);
for(int i = 2; i <= n; i++){
int u = g(t);
dp = t * t + query(1, 1, len, u);
l[++tot] = line(-2 * (t + L), (t + L) * (t + L) + dp);
update(1, 1, len, tot);
}
cout << dp;
return 0;
}
</code></pre>
<h2 id="p4655">P4655</h2>
<p>比较水的一道。</p>
<p>用 <span class="math inline">\(s_i\)</span> 表示 <span class="math inline">\(w_i\)</span> 的前缀和,那么可以列出一个 <span class="math inline">\(n^2\)</span> 的dp式子:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + (h_i - h_j)^2 + s_{i - 1} - s_j\}
\]</div><p></p><p>化简一下:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + h_i^2 - 2 \times h_i \times h_j + h_j^2 + s_{i - 1} - s_j\}
\]</div><p></p><p>把固定的给提出来可以得到:</p>
<p></p><div class="math display">\[\large dp_i = h_i^2 + s_{i-1} + \min_{j<i}\{-2h_j \times h_i + (h_j^2 - s_j + dp_j) \}
\]</div><p></p><p>注意到 <span class="math inline">\(\texttt{min}\)</span> 里面是以 <span class="math inline">\(-2h_j\)</span> 为 <span class="math inline">\(\texttt{k}\)</span>,<span class="math inline">\(h_j^2 - s_j + dp_j\)</span> 为 <span class="math inline">\(\texttt{b}\)</span>的一次函数,所以我们可以用李超线段树维护前面所有的一次函数,求 <span class="math inline">\(dp_i\)</span>时直接求 <span class="math inline">\(x = h_i\)</span> 时一次函数的最小值。复杂度优化到 <span class="math inline">\(O(nlogn)\)</span>。</p>
<h3 id="代码-1">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int M = 1e6 + 5;
int n, tot, h, w, sum, dp, tg;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x; b = y;
}
}l;
int f(int x, int id){
return l.k * x + l.b;
}
void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(mid, id) < f(mid, v)) swap(v, id);
if(f(l, id) < f(l, v)) update(2 * x, l, mid, id);
if(f(r, id) < f(r, v)) update(2 * x + 1, mid + 1, r, id);
}
int query(int x, int l, int r, int k){
if(r < k || l > k) return 1e18;
int ans = f(k, tg);
if(l == r) return ans;
int mid = (l + r) >> 1;
return min(ans, min(query(2 * x, l, mid, k), query(2 * x + 1, mid + 1, r, k)));
}
signed main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> h;
}
for(int i = 1; i <= n; i++){
cin >> w;
sum = sum + w;
}
l.b = 1e18;
l[++tot] = line(-2 * h, dp + h * h - sum);
update(1, 0, M, tot);
for(int i = 2; i <= n; i++){
dp = (h * h + sum) + query(1, 0, M, h);
l[++tot] = line(-2 * h, dp + h * h - sum);
update(1, 0, M, tot);
}
cout << dp;
return 0;
}
</code></pre>
<h2 id="p4983">P4983</h2>
<p>前置芝士:wqs二分,本题李超线段树复杂度比正解多一只 <span class="math inline">\(\texttt{log}\)</span>,需要卡卡常。</p>
<p>首先显然价值式子可以化简把,平均数消掉,变为<span class="math inline">\((\sum{x_i} + 1)^2\)</span>。</p>
<p>然后用 <span class="math inline">\(s\)</span> 表示前缀和,可以写出最朴素的 <span class="math inline">\(O(n^3)\)</span> 做法:</p>
<p></p><div class="math display">\[\large dp_{i,j} = \min_{k<i}\{dp_{k,j-1} + (s_i - s_k + 1)^2 \}
\]</div><p></p><p>注意到如果没有划分 <span class="math inline">\(m\)</span> 个的限制,写出复杂度正确的dp是容易的,所以这是典型的“恰好选k个”的题目,可以用wqs二分。</p>
<p>具体的,如果以划分数为横坐标 <span class="math inline">\(x\)</span>,划分数为 <span class="math inline">\(x\)</span> 时的答案为纵坐标 <span class="math inline">\(f(x)\)</span> 建立坐标系,在坐标系内画出点对 <span class="math inline">\((x, f(x))\)</span> 的连线,可以证明其是一个下凸的函数,所以切线斜率递增时,切点的横坐标也是递增的。所以可以二分斜率。</p>
<p>假设现在我们二分得到的一个新的斜率为 <span class="math inline">\(k\)</span>, 那么根据这个斜率可以得到哪些信息呢?根据 <span class="math inline">\(f(x) = k \times x + b\)</span>,所以截距 <span class="math inline">\(b = f(x) - k \times x\)</span>,由于这个函数是下凸的,所以过切点的那条直线截距是最小的。</p>
<p>那怎么求出最小值呢?由于现在的划分个数(也就是当前斜率对应直线切点的x)必须满足最小截距,也就是说现在是最小的截距决定当前的划分个数,所以我们可以不考虑划分个数限制直接做dp求最小值。</p>
<p>由于要求的是 <span class="math inline">\(f(x) - k \times x\)</span> 整体的最小值,所以在每次划分(也就是每次转移)的时候 -k 即可。同时在做dp时记录划分的个数,就可以把当前切点对应的 <span class="math inline">\(x\)</span> 给求出来。我们的目标是找到一个对应切点的横坐标等于 <span class="math inline">\(m\)</span> 的斜率,所以就可以根据当前的 <span class="math inline">\(x\)</span> 继续二分斜率。</p>
<p>假设最后找到的斜率是 <span class="math inline">\(res\)</span>,那么以 <span class="math inline">\(res\)</span> 为 k 再做一次dp,再加上 <span class="math inline">\(res \times m\)</span> 就是最终的答案了。</p>
<p>最后考虑如何做 dp,由于现在我们不需要考虑划分数的限制,所以直接写出 <span class="math inline">\(n^2\)</span> 式子:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + (s_i - s_j + 1)^2 \} - k
\]</div><p></p><p>拆开得到:</p>
<p></p><div class="math display">\[\large dp_i =(s_i + 1)^2 +\min_{j<i}\{-2s_js_i + s_j^2 - 2s_j + dp_j \} - k
\]</div><p></p><p>使用李超线段树即可,复杂度 <span class="math inline">\(O(nlognlogV)\)</span>, <span class="math inline">\(V\)</span>为wqs二分的值域,在这道题为<span class="math inline">\(1e18\)</span>;<span class="math inline">\(n\)</span>为<span class="math inline">\(1e5\)</span>,算下来大概是<span class="math inline">\(1e8\)</span>,卡卡能过。</p>
<h3 id="代码-2">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
#define pi pair<int, int>
using namespace std;
const int N = 1e5 + 5;
int n, m, len, a, sum, tg, dp, cnt;
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-48;ch=getchar();}
return x*f;
}
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x, b = y;
}
}l;
inline pi min(pi a, pi b){
if(a.first < b.first) return a;
return b;
}
inline int g(int x){
return lower_bound(a + 1, a + len + 1, x) - a;
}
inline int f(int x, int id){
return l.k * x + l.b;
}
inline void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(a, id) < f(a, v)) swap(id, v);
if(f(a, id) < f(a, v)) update(2 * x, l, mid, id);
if(f(a, id) < f(a, v)) update(2 * x + 1, mid + 1, r, id);
}
inline pi query(int x, int l, int r, int k){
pi out;
out.second = tg;
out.first = f(a, tg);
if(l == r) return out;
int mid = (l + r) >> 1;
if(k <= mid) return min(out, query(2 * x, l, mid, k));
return min(out, query(2 * x + 1, mid + 1, r, k));
}
inline int check(int k){
memset(tg, 0, sizeof(tg));
for(int i = 1; i <= n; i++){
pi out = query(1, 1, len, g(sum));
dp = (sum + 1) * (sum + 1) + out.first - k;
l = line(-2 * sum, sum * sum - 2 * sum + dp);
update(1, 1, len, i);
cnt = cnt + 1;
}
return cnt;
}
signed main(){
n = read(); m = read();
for(int i = 1; i <= n; i++){
sum = read();
sum += sum;
a = sum;
}
sort(a + 1, a + n + 1);
len = unique(a + 1, a + n + 1) - a - 1;
int l = -1e18, r = 0;
while(l <= r){
int mid = (l + r) >> 1;
if(check(mid) >= m) r = mid - 1;
else l = mid + 1;
}
check(r);
cout << dp + m * r;
return 0;
}
</code></pre>
<h2 id="p5785">P5785</h2>
<p>双倍经验:P10979</p>
<p>这道题的 <span class="math inline">\(O(n^2)\)</span> 的做法还是挺不好想的,这里不赘述,可以看弱化版 P2365 的题解。</p>
<p>大概思路是先把每个 <span class="math inline">\(s\)</span> 的代价提前计算,再加上前缀和优化。</p>
<p>用 <span class="math inline">\(sumf\)</span> 和 <span class="math inline">\(sumt\)</span> 表示 <span class="math inline">\(f\)</span> 和 <span class="math inline">\(t\)</span> 的前缀和,那么可以写出 <span class="math inline">\(n^2\)</span> 做法:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + sumt_i \times (sumf_i - sumf_j) + s \times (sumf_n - sumf_j) \}
\]</div><p></p><p>化简一下就可以很容易化出一次函数的形式:</p>
<p></p><div class="math display">\[\large dp_i = s \times sumf_n + sumt_i \times sumf_i + \min_{j<i}\{-sumf_j * sumt_i + dp_j - s \times sumf_j \}
\]</div><p></p><p>果断使用李超线段树,需要离散化或动态开点。</p>
<h3 id="代码-3">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 5;
int n, s, len, tot, f, t, sumf, sumt, a, tg, dp;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x; b = y;
}
}l;
int g(int x){
return lower_bound(a + 1, a + len + 1, x) - a;
}
int F(int x, int id){
return l.k * x + l.b;
}
void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(F(a, id) < F(a, v)) swap(id, v);
if(F(a, id) < F(a, v)) update(2 * x, l, mid, id);
if(F(a, id) < F(a, v)) update(2 * x + 1, mid + 1, r, id);
}
int query(int x, int l, int r, int k){
if(l > k || r < k) return 1e18;
int ans = F(a, tg);
if(l == r) return ans;
int mid = (l + r) >> 1;
if(k <= mid) return min(ans, query(2 * x, l, mid, k));
return min(ans, query(2 * x + 1, mid + 1, r, k));
}
signed main(){
cin >> n >> s;
for(int i = 1; i <= n; i++){
cin >> t >> f;
sumt = sumt + t;
sumf = sumf + f;
a = sumt;
}
sort(a + 1, a + 1 + n);
len = unique(a + 1, a + n + 1) - a - 1;
for(int i = 1; i <= n; i++){
dp = sumf * s + sumf * sumt + query(1, 1, len, g(sumt));
l[++tot] = line(-sumf, dp - sumf * s);
update(1, 1, len, tot);
}
cout << dp;
return 0;
}
</code></pre>
<h2 id="p5896">P5896</h2>
<p>wqs二分被称为"Aliens Trick"的出处。所以显然需要前置芝士wqs二分(我这里面应该有一篇详细讲了一下,这道题就不详细讲wqs二分了)。</p>
<p>考虑一个坐标为<span class="math inline">\((x, y)\)</span>的兴趣点会被怎样的拍摄区域覆盖:对于每一个正方形,用 <span class="math inline">\(l\)</span> 表示 <span class="math inline">\(min(x, y)\)</span>, <span class="math inline">\(r\)</span> 表示 <span class="math inline">\(max(x, y)\)</span>,可以发现如果有一个左上角方格坐标为 <span class="math inline">\((L, L)\)</span> ,右下角坐标为 <span class="math inline">\((R, R)\)</span> 的拍摄区域(因为一定在主对角线上,所以这两个方格的横纵坐标相等),当 <span class="math inline">\(L \le l\)</span> 且 $ R \ge r$ 时这个拍摄区域可以覆盖这个兴趣点。</p>
<p>这时我们就可以转化一下题意:数轴上有 <span class="math inline">\(n\)</span> 个范围分别为 <span class="math inline">\((l_i, r_i)\)</span> 的区间,你要新建 <span class="math inline">\(k\)</span> 个区间去覆盖一开始的区间,新建一个覆盖范围为 <span class="math inline">\((L, R)\)</span> 的区间的代价为 <span class="math inline">\((R - L + 1) ^ 2\)</span>,求最小代价。</p>
<p>显然在一开始就已经被其他区间覆盖的区间是没有用的,因为覆盖它的区间被覆盖它也就会被覆盖。类似于P6047丝之割,把没用的区间去除,剩下的区间 <span class="math inline">\(l\)</span> 单调递增,<span class="math inline">\(r\)</span> 也单调递增。考虑wqs二分去掉 <span class="math inline">\(k\)</span> 个限制,写出 <span class="math inline">\(dp\)</span> 式子:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j < i}\{dp_j + (r_i - l_{i + 1} + 1)^2 - max(0, r_j - l_{j + 1} + 1)^2_{(\texttt{这里是去除重复覆盖})} \} - K_{wqs}
\]</div><p></p><p>展开:</p>
<p></p><div class="math display">\[\large dp_i = (r_i + 1)^2 + \min_{j < i}\{-2l_{j+1}(r_i + 1) + dp_j + l_{j + 1}^2 - max(0, r_j - l_{j+1} + 1)^2 \} - K
\]</div><p></p><p>显然可以斜率优化。由于本题时限为2s所以李超线段树随便过。本题可以不用离散化。</p>
<h3 id="代码-4">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define pi pair<int, int>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, k;
int ans, tot, cnt, len, tg, dp, s, a;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x, b = y;
}
}l;
struct node{
int l, r;
}b, c;
inline bool cmp(node a, node b){
if(a.l == b.l) return a.r > b.r;
return a.l < b.l;
}
inline int g(int x){
return lower_bound(a + 1, a + len + 1, x) - a;
}
inline int f(int x, int id){
return l.k * x + l.b;
}
inline void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(a, id) < f(a, v)) swap(v, id);
if(f(a, id) < f(a, v)) update(2 * x, l, mid, id);
if(f(a, id) < f(a, v)) update(2 * x + 1, mid + 1, r, id);
}
inline pi query(int x, int l, int r, int k){
pi out;
out.first = f(a, tg);
out.second = tg;
if(l == r) return out;
int mid = (l + r) >> 1;
if(k <= mid) return min(out, query(2 * x, l, mid, k));
else return min(out, query(2 * x + 1, mid + 1, r, k));
}
inline bool check(int K){
memset(tg, 0, sizeof(tg));
l = line(-2 * c.l, dp + c.l * c.l);
update(1, 1, len, 0);
for(int i = 1; i <= n; i++){
pi q = query(1, 1, len, g(c.r + 1));
dp = (c.r + 1) * (c.r + 1) + q.first - K;
s = s + 1;
l = line(-2 * c.l, dp + c.l * c.l - max((long long)0, (c.r - c.l + 1)) * max((long long)0, (c.r - c.l + 1)));
update(1, 1, len, i);
}
if(s <= k) ans = dp + k * K;
return s <= k;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; i++){
int x, y;
cin >> x >> y;
x++; y++;
b.l = min(x, y);
b.r = max(x, y);
}
sort(b + 1, b + n + 1, cmp);
for(int i = 1; i <= n; i++){
if(b.r > c.r){
c[++tot] = b;
a[++cnt] = c.r + 1;
}
}
len = unique(a + 1, a + cnt + 1) - a - 1;
n = tot;
int L = -1 * m * m, R = 0;
while(L <= R){
int mid = (L + R) >> 1;
if(check(mid)) L = mid + 1;
else R = mid - 1;
}
check(R);
cout << ans;
return 0;
}
</code></pre>
<h2 id="p6047">P6047</h2>
<p>是我最喜欢的丝之歌耶( •̀ ω •́ )y</p>
<p>由于只能从左往右切割,那么可以注意到对于交叉的两根线,切割了左斜的那一条就一定能切割右斜的那一条。即对于题目中的图来说:<br>
<img src="https://img2024.cnblogs.com/blog/3695173/202602/3695173-20260206141340401-430180296.webp"><br>
对于左边的两根线来说,以上端点从左至右排序第一根线被切了,那么第二根也一定会被切掉。</p>
<p>由于所有的线都要切,所以类似于上图第二根线这样的线完全不用考虑,所以可以考虑先把这些线去掉。</p>
<p>由于交叉的线都去掉了一根,所以去掉之后的图案应该是左端点和右端点都单调递增的,那么就可以写出dp式子:</p>
<p></p><div class="math display">\[\large dp_i = \min_{j<i}\{dp_j + \min^{j - 1}_{k=1}\{a_k \} \times \min^n_{k=i + 1}\{b_k \} \}
\]</div><p></p><p>后面的两个最小值都是可以预处理出来的,可以发现其实是一个一次函数,用李超线段树维护,每次查询查 <span class="math inline">\(x=\min^n_{k=i + 1}\{b_k \}\)</span> 处的最小值即可。</p>
<h3 id="代码-5">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 5;
const int M = 1e6 + 5;
int n, m, cnt, tot, a, b, tg, ma, mb, dp;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x; b = y;
}
}l;
struct silk{
int u, v;
}song, s;
bool cmp(silk x, silk y){
if(x.u == y.u) return x.v > y.v;
return x.u < y.u;
}
void prework(){
sort(song + 1, song + m + 1, cmp);
for(int i = 1; i <= m; i++){
if(song.v > s.v) s[++cnt] = song;
}
ma = a;
for(int i = 2; i <= n; i++){
if(a < ma) ma = a;
else ma = ma;
}
mb = b;
for(int i = n - 1; i >= 1; i--){
if(b < mb) mb = b;
else mb = mb;
}
l.b = 1e18;
}
int f(int x, int id){
return l.k * x + l.b;
}
void update(int x, int l, int r, int id){
int &v = tg; int mid = (l + r) >> 1;
if(f(mid, id) < f(mid, v)) swap(id, v);
if(f(l, id) < f(l, v)) update(2 * x, l, mid, id);
if(f(r, id) < f(r, v)) update(2 * x + 1, mid + 1, r, id);
}
int query(int x, int l, int r, int k){
if(l > k || r < k) return 1e18;
int ans = f(k, tg);
if(l == r) return ans;
int mid = (l + r) >> 1;
return min(ans, min(query(2 * x, l, mid, k), query(2 * x + 1, mid + 1, r, k)));
}
signed main(){
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a;
for(int i = 1; i <= n; i++) cin >> b;
for(int i = 1; i <= m; i++) cin >> song.u >> song.v;
prework();
for(int i = 1; i <= cnt; i++){
l[++tot] = line(ma.u - 1], dp);
update(1, 0, M, tot);
dp = query(1, 0, M, mb.v + 1]);
}
cout << dp;
return 0;
}
</code></pre>
<h2 id="p6246">P6246</h2>
<p>前置芝士:wqs二分。</p>
<p>根据小学芝士对于每一个区间修在中间是最优的,用前缀和优化可以 <span class="math inline">\(O(1)\)</span> 计算代价,然后就可以过掉绿题版本。</p>
<p>加上一个裸的wqs二分可以以 <span class="math inline">\(1e8\)</span> 的理论复杂度,实际最慢的点也不超过400ms的速度过掉紫题版本(数据真水)。</p>
<p>对于本题,先wqs二分去掉选m个的限制,然后抛弃修在中间是最优的想法(那样不好斜率优化)。这样写出一个 <span class="math inline">\(O(n^3)\)</span>的dp柿子(s为前缀和):</p>
<p></p><div class="math display">\[\large dp_i = \min_{j \le i}\{(s_i-s_j)-(i-j)\times a_j + \min_{k<j}\{(j-k)\times a_j - (s_j-s_k) \}\}
\]</div><p></p><p>设 <span class="math inline">\(f_i = \min_{k<j}\{(j-k)\times a_j - (s_j-s_k) \}\}\)</span>,发现可以斜率优化求得。</p>
<p>然后发现<span class="math inline">\(dp_i = \min_{j \le i}\{(s_i-s_j)-(i-j)\times a_j + f_j \}\)</span>,也可以斜率优化。</p>
<h3 id="代码-6">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
#define pi pair<int, int>
using namespace std;
const int N = 2e6 + 5;
int n, m, ans, mx, a, sum, dp, f, cnt1, cnt2;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x, b = y;
}
};
struct lctree{
line l;
int tg;
inline int f(int x, int id){
return l.k * x + l.b;
}
inline void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(mid, id) < f(mid, v)) swap(v, id);
if(f(l, id) < f(l, v)) update(2 * x, l, mid, id);
if(f(r, id) < f(r, v)) update(2 * x + 1, mid + 1, r, id);
}
inline pi query(int x, int l, int r, int k){
pi out;
out.first = f(k, tg);
out.second = tg;
if(l == r) return out;
int mid = (l + r) >> 1;
if(k <= mid) return min(out, query(2 * x, l, mid, k));
else return min(out, query(2 * x + 1, mid + 1, r, k));
}
}tr1, tr2;
inline bool check(int k){
memset(tr1.tg, 0, sizeof(tr1.tg));
memset(tr2.tg, 0, sizeof(tr2.tg));
for(int i = 1; i <= n; i++) f = cnt1 = cnt2 = 0;
for(int i = 1; i <= n; i++){
pi q = tr2.query(1, 1, mx + 1, a);
f = i * a -sum + q.first;
cnt2 = cnt1;
tr1.l = line(-a, i * a - sum + f);
tr1.update(1, 1, n, i);
q = tr1.query(1, 1, n, i);
dp = sum + q.first - k;
cnt1 = cnt2 + 1;
tr2.l = line(-i, sum + dp);
tr2.update(1, 1, mx + 1, i);
}
if(cnt1 <= m) ans = dp + m * k;
return cnt1 <= m;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a;
mx = max(mx, a);
sum = sum + a;
}
int l = -2e6, r = 0, res;
while(l <= r){
int mid = (l + r) >> 1;
if(check(mid)) res = mid, l = mid + 1;
else r = mid - 1;
}
check(res);
cout << ans;
return 0;
}
</code></pre>
<h2 id="p8726">P8726</h2>
<p>斜率优化水题,<span class="math inline">\(n^2\)</span> 的式子及其明显:</p>
<p></p><div class="math display">\[\large dp_i = \max_{j<i}\{T_i \times T_j + \lfloor\frac{dp_j}{2}\rfloor\ - F_j \}
\]</div><p></p><p>然后发现这个式子本身就是一次函数,不需要做任何化简就可以直接套李超线段树。</p>
<h3 id="代码-7">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 5;
const int M = 2e4 + 5;
int n, tot, t, f, dp, tg, ans;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x; b = y;
}
}l;
int F(int x, int id){
return l.k * x + l.b;
}
void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(F(mid, id) > F(mid, v)) swap(id, v);
if(F(l, id) > F(l, v)) update(2 * x, l, mid, id);
if(F(r, id) > F(r, v)) update(2 * x + 1, mid + 1, r, id);
}
int query(int x, int l, int r, int k){
if(l > k || r < k) return -1e18;
int res = F(k, tg);
if(l == r) return res;
int mid = (l + r) >> 1;
return max(res, max(query(2 * x, l, mid, k), query(2 * x + 1, mid + 1, r, k)));
}
signed main(){
cin >> n;
for(int i = 1; i <= n; i++) cin >> t;
for(int i = 1; i <= n; i++) cin >> f;
l.b = -1e18;
l[++tot] = line(t, dp / 2 - f);
update(1, 0, M, tot);
for(int i = 2; i <= n; i++){
dp = query(1, 0, M, t);
ans = max(ans, dp);
l[++tot] = line(t, dp / 2 - f);
update(1, 0, M, tot);
}
cout << ans;
return 0;
}
</code></pre>
<h1 id="直接维护直线或线段的应用">直接维护直线或线段的应用</h1>
<p>这些题可以把操作转化为维护一些直线或线段,然后直接用李超线段树。</p>
<h2 id="p11728">P11728</h2>
<p>容易发现每一个机器人离原点的距离是关于 <span class="math inline">\(t\)</span> 的一次函数。也就是说,以时间为横坐标,离原点的距离为纵坐标建立平面直角坐标系,可以用一条条线段(也就是有定义域限制的一次函数)。</p>
<p>那现在有 <span class="math inline">\(\texttt{command}\)</span> 操作,线段变成了一段折线,又该怎么做呢?很容易发现,折线中的每一段还是一条条限制了定义域的一次函数,所以本质上没区别。</p>
<p>为了确定每一条线段的定义域,需要把操作离线,然后对于每一个机器人分别操作,把它对应的折线一段一段加进去。</p>
<p>注意由于是离原点的距离, <span class="math inline">\(y\)</span> 的负半轴也要考虑,所以函数求值时要加绝对值。另外 <span class="math inline">\(t\)</span> 的值域过大,<s>但是我不想写动态开点</s>,所以写了离散化。</p>
<h3 id="代码-8">代码</h3>
<pre><code>#include<bits/stdc++.h>
#define int long long
#define pi pair<int, int>
using namespace std;
const int N = 6e5 + 5;
int n, m, maxt, len, tot, c, a, tg, dp;
vector<int> ask;
vector<pi> cg;
struct line{
int k, b;
line(int x = 0, int y = 0){
k = x; b = y;
}
}l;
int g(int x){
return lower_bound(a + 1, a + len + 1, x) - a;
}
int f(int x, int id){
return abs(l.k * x + l.b);
}
void update(int x, int l, int r, int id){
int &v = tg;
int mid = (l + r) >> 1;
if(f(a, id) > f(a, v)) swap(id, v);
if(f(a, id) > f(a, v)) update(2 * x, l, mid, id);
if(f(a, id) > f(a, v)) update(2 * x + 1, mid + 1, r, id);
}
void modify(int x, int l, int r, int cl, int cr, int id){
if(r < cl || l > cr) return;
if(l >= cl && r <= cr) update(x, l, r, id);
else {
int mid = (l + r) >> 1;
modify(2 * x, l, mid, cl, cr, id);
modify(2 * x + 1, mid + 1, r, cl, cr, id);
}
}
int query(int x, int l, int r, int k){
int ans = f(a, tg);
if(l == r) return ans;
int mid = (l + r) >> 1;
if(k <= mid) return max(ans, query(2 * x, l, mid, k));
return max(ans, query(2 * x + 1, mid + 1, r, k));
}
signed main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> c;
cg.push_back({0, 0});
}
a = 0;
for(int i = 1; i <= m; i++){
int t, k, x;
string s;
cin >> t >> s;
maxt = max(maxt, t);
a = t;
if(s == 'c'){
cin >> k >> x;
if((*cg.rbegin()).first == t) cg.pop_back();
cg.push_back({t, x});
}
else ask.push_back(t);
}
m++;
sort(a + 1, a + 1 + m);
len = unique(a + 1, a + m + 1) - a - 1;
for(int i = 1; i <= n; i++){
cg.push_back({maxt + 1, 0});
int h = c;
for(int j = 0; j < cg.size() - 1; j++){
l[++tot] = line(cg.second, h - cg.first * cg.second);
modify(1, 0, len + 1, g(cg.first), g(cg.first - 1), tot);
h += (cg.first - cg.first) * l.k;
}
}
for(int i = 0; i < ask.size(); i++){
cout << query(1, 0, len + 1, g(ask)) << '\n';
}
return 0;
}
</code></pre><br><br>
来源:https://www.cnblogs.com/-114514/p/19581536
頁:
[1]