Fork me on GitHub

2025.5.26~2025.5.29 总结

做了 # P3642 [APIO2016] 烟花表演 本来是想用 \(slope trick\) 来做的,结果一打开题解,发现还有可并堆的做法,这不巧了吗,最近刚学,所以打算明天写一发可并堆,然后一起总结了。

这是2025.5.27的后续,首先,先想吐槽的是,题解能不能写清楚啊!我认为的是纯可并堆,但是,其实是 \(slope trick\) ! 然后用可并堆维护,求你了!下次写清楚一点吧!这道题真的代码很简单,只是有点难推而已。

那么我们可以设计一个非常暴力的dp状态就是dp(u,i)表示将u子树中的所有叶子节点调整至距u的父亲距离为j的最小代价

那么我们的转移方程也是十分的平凡的,枚举u和父亲的边到底变成了什么值然后进行取min操作

可以推出这样的一个式子: latex不打了!

详见

我们发现我们处理的函数是一个类似于min卷积一样的式子, 显然快速的计算这个东西是没什么可能的……

不过我们的f(u,x)这个函数相当的有特点,在接下来我们将会使用归纳法证明f(u,x)是一个关于x的分段函数并且在每段都是线性的

那么对于函数的min卷积这个运算,尽管一般情况下是没有什么性质的,但是在这道题里我们有一个函数是绝对值函数,而另一个函数是一个简单的分段函数

此时我们就有可能会有一些性质了

那么我们手动分情况大力讨论一波……(想证明的人自己手丸去吧,推式子推了我一个下午)

会发现min卷积大概有这么几个性质

1.一个斜率比-1大的直线卷积上斜率为-1的直线之后新函数截距为两个函数的截距之和,斜率不变

2.一个常数函数和一个斜率为-1的直线卷积之后截距为两个函数截距之和而斜率变为-1

3.对于一个下凸的分段函数并且每一段的斜率都大于1,和一个斜率为+1的直线卷积之后,这个函数将会变成一条斜率为1的直线

事实上刚才的结论只是一个感性理解,因为我们在卷积的时候远不止这几种情况

然后我们胡乱爆推一波结论之后可以得到这个式子,如果我们给f(u,x)这个函数添加一条父亲边的话我们函数会做这样的变化,假设这个函数在[L,R]处取值为0

1.将函数在[1,L]的一段向上平移val个单位

2.将函数在[R,+∞]的一段向右平移val个单位并且斜率改为+1

3.将函数在[L,R]的一段向右平移val个单位

4.此时你发现中间空出了一段斜率为-1,val个单位高val个单位宽的线段,在中间插入一个线段就行了

合并相邻的子树就是将函数简单的相加起来

那么我们采用这样的方式来存储一个函数f(u,x)

通过数学归纳法可以得到f(u,x)的最右端斜率等于1,而在插入父亲之前函数的最左段斜率=u的儿子数目

我们现在通过存储这个分段函数的每一个拐点的x坐标来表示这个函数

并且我们认为,从右向左,每经过一个拐点,函数的斜率-1

那么初始的时候我们需要存储一个绝对值函数,这样的话我们就用两个在同一个位置的拐点来描述这个绝对值函数

这样储存函数有一个好处就是当我们把函数相加的时候只需要简易的将两个函数的拐点列表合并一下再将最右端斜率简单相加就可以完成函数的合并工作了

那么我们现在已经可以完成函数相加的操作了,我们现在需要支持的操作就是将函数所有斜率为正的拐点全部弹出这个操作了(至于修改斜率什么的其实简单的就是插入两个拐点就行了)

那么我们发现其实这个操作就等价于弹出函数中x坐标前k大的拐点其中 k 为我们当前处理的点的孩子个数

那么这个操作很显然就是删除k次最大值

又发现我们之前将两个函数相加的操作就是将两个函数的拐点列表合并

所以我们写一个可并的大根堆(或者直接使用pb_ds)就可以完成合并函数和插入一条边的操作了

最后我们得到了根节点的函数,显然 也就是所有边的和,而最右段的斜率就是根节点的度数,我们还知道每经过一个点函数的斜率就会-1,因此根据这3点我们就可以反推出整个函数的表达式

然后就可以做了,提取一下这个分段函数斜率为0的那一段值就ok了

代码如下

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 3e5 + 10;

int n, m, ans;
int fa[N], w[N], deg[N];
int val[N << 1], d[N << 1], rt[N], tot;
int lc[N << 1], rc[N << 1];

int create(int v)
{
    val[++tot] = v, d[tot] = 0;
    return tot;
}
int merge(int p, int q)
{
    if(p == 0 || q == 0) return p + q;
    if(val[p] < val[q]) swap(p, q);
    rc[p] = merge(rc[p], q);
    if(d[lc[p]] < d[rc[p]]) swap(lc[p], rc[p]);
    d[p] = d[rc[p]] + 1;
    return p;
}

void pop(int &p)
{
    p = merge(lc[p], rc[p]); 
}

signed main() 
{
    cin >> n >> m;
    for (int i = 2; i <= n + m; i++)
    {
        cin >> fa[i] >> w[i];
        ans += w[i];
        deg[fa[i]] ++ ;
    }
    for(int u = n + m ; u >= 2 ; u -- ) 
    {
        if(u <= n) while (deg[u]-- > 1) pop(rt[u]);
        int R = val[rt[u]];
        pop(rt[u]);
        int L = val[rt[u]];
        pop(rt[u]);
        rt[u] = merge(rt[u], merge(create(L + w[u]), create(R + w[u])));
        rt[fa[u]] = merge(rt[fa[u]], rt[u]);
    }
    while(deg[1] -- ) pop(rt[1]);
    while(rt[1])
    {
        ans -= val[rt[1]];
        pop(rt[1]);
    }
    cout << ans;
    return 0;
}

还做了 # CF1707E Replace,其实就是 \(\text{Chy}\) 上周模拟赛的T1啦,其实就是倍增,不过真的很难想到,刚刚看了一眼,还能交题解诶,明天水一发,总结也一起写吧!

好说好说,这道 3500 真的不太难(至少代码不太难,结论也不太难

这是一看就能看出来的倍增吧,我们只要维护 \([l, r]\) 被操作 \(2^k\) 后的区间就好了吧,但是,这区间也太多了吧!这可不行呀,那我们就得考虑考虑,该怎么把一个区间分成若干个区间,还能保证,操作之后合并起来是原区间的结果呢?

我们发现 \(f(l \cup r) = f(l) \cup f(r)\),那么如果我们想要维护 \(f(l,r)\),其实只要维护 \(f(l, l+1),f(l+1, l+2) \dots f(r -1,r)\),然后 \(f(l,l + 1) \cup f(l + 1, l + 2) \dots \cup f(r - 1, r)\) 就是 \(f(l,r)\) 了,所以不用维护 \([l, r]\) 只要维护 \([i,i+1]\) 就可以啦,转移就是先设 \([l, r] = f^{2^{k-1}}(i, i+1)\)求 $ \overset{r-1}{\underset{i=l}{\cup}}(i, i+1)$ 就可以了,查询也是一样的吧。那这个就用 \(st\) 表来维护就好了!

时间复杂度:\(O(n\log^2n + q\log_{}n)\)

代码不好看,但是还是放放:

#include <bits/stdc++.h>

#define int long long
#define x first
#define y second

using namespace std;

const int N = 1e5 + 10;

int n, q;
pair<int, int> f[22][N][22];
int lg[N];

pair<int, int> query(int l, int r, int id)
{
    if(l >= r) return make_pair(n + 1, 0);
    int k = lg[r - l];
    return make_pair(min(f[id][l][k].x, f[id][r - (1 << k)][k].x), max(f[id][l][k].y, f[id][r - (1 << k)][k].y));
}

int query(int l, int r) 
{
    if(l == 1 && r == n) return 0;
    pair<int, int> ans = query(l, r, 20);
    int res = 0;
    if(ans.x != 1 || ans.y != n) return -1;
    for(int i = 19 ; i >= 0 ; i -- )
    {
        ans = query(l, r, i);
        if(ans.x != 1 || ans.y != n)
        {
            l = ans.x;
            r = ans.y;
            res += 1 << i;
        }
    }
    return res + 1;
}

int a[N];

void init()
{
    for(int j = 1 ; j < n ; j ++ ) f[0][j][0] = make_pair(min(a[j], a[j + 1]), max(a[j], a[j + 1]));
    for(int k = 1 ; k <= 20 ; k ++ )
        for(int j = 1 ; j + (1 << k) <= n ; j ++ ) 
            f[0][j][k].y = max(f[0][j][k - 1].y, f[0][j + (1 << k - 1)][k - 1].y), f[0][j][k].x = min(f[0][j][k - 1].x, f[0][j + (1 << k - 1)][k - 1].x);
    for(int i = 1 ; i <= 20 ; i ++ )
    {
        for(int j = 1 ; j < n ; j ++ ) f[i][j][0] = query(f[i - 1][j][0].x, f[i - 1][j][0].y, i - 1);
        for(int k = 1 ; k <= 20 ; k ++ )
            for(int j = 1 ; j + (1 << k) <= n ; j ++ ) 
                f[i][j][k].y = max(f[i][j][k - 1].y, f[i][j + (1 << k - 1)][k - 1].y), f[i][j][k].x = min(f[i][j][k - 1].x, f[i][j + (1 << k - 1)][k - 1].x);
    }
}

signed main()
{
    cin >> n >> q;
    for(int i = 1 ; i <= n ; i ++ ) cin >> a[i];
    for(int i = 2 ; i <= n ; i ++ ) lg[i] = lg[i >> 1] + 1;
    init();
    while(q -- )
    {
        int l, r;
        cin >> l >> r;
        cout << query(l, r) << endl;
    }
    return 0;
}
posted @ 2025-06-04 15:24  tony0530  阅读(28)  评论(0)    收藏  举报