2021.牛客暑假多校 第七场 补题

  1. B 题 https://ac.nowcoder.com/acm/contest/11258/B 线段树

洛谷上有原题,先来看看洛谷上那道题: https://www.luogu.com.cn/problem/P4198 题意有点小问题

大意:有 n 栋楼,看做放在直角坐标系中,第 i 栋楼的横坐标为 i,m 次操作,每次可以将修改楼的纵坐标。现有个人在 (0,0)除,某栋楼能被看到的条件是,该楼最高点和(0,0)的连线不会和在他前面所有的楼的最高点和(0,0) 的连线相交或重合。没每次修改过后,这个人能看到多少栋楼。(1<=n,m<=1e5)

思路:对于某栋楼能被看到提炼出来显然就是:该楼与(0,0)连线的斜率,比之前的都大。所以我们查询就是相当于在问从1开始的[1,n]中递增的斜率有多少个。因为查询和修改都是1e5级别。我们可以用线段树做。因为是单点修改,pushdown操作是不用写的。我们线段树中存下该区间从区间起点开始斜率递增的个数和区间斜率最大值。这个题难就难在pushup操作怎么写,显然我们没办法O(1)的简单合并讲个子区间。对于已经处理好的两个子节点的值,我们合并这两个左右区间 x, y 的时候…先来明确几个点:对于每个节点我们存的是从当前节点的左区间开始的递增的个数。y 的 左右子节点用 l,r 表示。s表示是节点中的存的个数,max 表示节点中存的最大值。回归正题:上面我们说到不能简单的合并,那我们该怎么合并呢? 对于合并 x, y 的时候,我们最终的结果是 从 x节点 的左端点开始的递增的个数。对于 y 中,显然是大于 x 中的最大值的部分,这也是我们为什么要在每个节点都存个最大值的原因。对于 y 我们分别考虑左右子节点 l,r。 若 lmax>xmax,显然 l 中最大那个对合并一定是有贡献的, r 的贡献部分,我们不仅得 >xmax 还得保证选的是 >lmax 的部分,取交集就是>lmax。并且我们 y 中的个数是算好了的,在 y 中, 我们 r 的贡献恰好就是 r 中 >lmax 的个数,所以对于 lmax>xmax 的时候,我们 r 的部分是可以直接算的,直接 sy-sl 就好了。然后接着递归 l 。对于 lmax<=xmax,显然 l 这部分是不行的,直接递归处理 r 就好了。实在看不懂的话搞个样例试一下很容易就能明白了。总的时间复杂度就是 O ( n ( l o g n ) 2 ) O(n(logn)^2) O(n(logn)2)

详细见代码:

#include <bits/stdc++.h>
using namespace std;
const int N=100010;
struct node{
    int l,r,s;
    double mx;
}tr[N*4];
int n,m;
void build(int u,int l,int r)
{
    tr[u]={l,r,0,0};
    if(l!=r)
    {
        int mid=l+r >> 1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
    }
}
void pushup1(int u)
{   
    tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
int pushup2(double lx,int u) 
{
    if(tr[u].mx<=lx)return 0;
    if(tr[u].l==tr[u].r)return tr[u].mx>lx;
    int x=u<<1,y=u<<1|1;
    if(tr[x].mx<=lx)return pushup2(lx,y);
    else return tr[u].s-tr[x].s+pushup2(lx,x);
}
void modify(int u,int x,double y)
{
    if(tr[u].l==x&&tr[u].r==x)
    {
        tr[u].s=1;tr[u].mx=y;
        return ;
    }
    int mid=tr[u].l+tr[u].r >> 1;
    if(x<=mid)modify(u<<1,x,y);
    if(x>mid)modify(u<<1|1,x,y);
    pushup1(u);
    tr[u].s=tr[u<<1].s+pushup2(tr[u<<1].mx,u<<1|1);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    build(1,1,n);
    while(m--)
    {
        int x,y;
        cin>>x>>y;
        modify(1,x,(double)y/x);
        cout<<tr[1].s<<"\n";
    }
    return 0;
}

然后再来看这道题:大意:给定 a,b 两个序列,单点修改 a 序列,区间修改 b 序列。每次查询 [l,r] , 问 a 序列中满足从 l 开始 非严格递增的子序列的下标在 b 序列中对应的数构成的新的集合, b i bi bi b i + 1 b_{i+1} bi+1 奇偶性不同的个数。

这题实际上就上上一题的升级版,和上一道题类似,只不过线段树节点多维护个信息就好了。多维护个最大值mx的位置在 b 序列中对应的数是什么,因为最后计算个数的时候总是要递归到叶子节点计算的,注意每次到了叶子节点更新下存的 b 的值以及最大值就好了。所以存好信息后和上面更新方式是差不多的。对于线段树无非是多了个 pushdown操作。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=200010;
struct node{
    int l,r;
    int mx,cnt,bt,lazy;
    #define m(u) tr[u].mx
    #define c(u) tr[u].cnt
    #define b(u) tr[u].bt
    #define lz(u) tr[u].lazy
}tr[N*4];
int n,m,a[N],b[N];
void pushdown(int u)
{
    if(lz(u))
    {
        lz(u)=0;
        lz(u<<1)^=1,b(u<<1)^=1;
        lz(u<<1|1)^=1,b(u<<1|1)^=1;
    }
}
//在子树k中以mx为起点的贡献,bt为mx位置的b的值
int cale(int u,int &mx,int &bt)//这里的 mx,bt 要通过引用实时更新。因为一旦左儿子可以的话一定去先算左儿子,宏观上看就是从左往右扫,实时更新mx,显然就得递增的
{
    if(m(u)<mx)return 0;       //简单版的没有实时更新mx,但是也是对的。因为我们只是求的递增的个数,每个节点存的信息就是从当前代表的区间左端点开始的递增个数
    if(tr[u].l==tr[u].r)       //即每个区间内维护的就是递增的,也就是说在用 x,y 两个儿子更新 u 的时候, y 里面存的就是递增的,本来就是合法的。无论怎么递归,只需要和 x 比就好了
    {
        if(m(u)>=mx)           // 我们这里的不一样,我们求的不是递增的个数,而是从 b 中提取出来的新序列相邻两个数奇偶性不同的个数,所以必须实时更新。更准确的说应该是为了更新bt,需要更新mx,可以用引用进行实时更新
        {
            int ans=bt^b(u);  // bt相当于纪录的是上一个  
            mx=m(u),bt=b(u);
            return ans;
        }
        else return 0;
    }
    pushdown(u); 
    if(m(u<<1)<mx)return cale(u<<1|1,mx,bt); 
    else
    {
        int ans=c(u)-c(u<<1)+cale(u<<1,mx,bt); 
        mx=m(u),bt=b(u); // 因为这里右儿子直接算出来了,相当于直接把整个父区间算好了 
        return ans;
    }
}
void pushup(int u)
{
    if(m(u<<1)>m(u<<1|1))m(u)=m(u<<1),b(u)=b(u<<1);
    else m(u)=m(u<<1|1),b(u)=b(u<<1|1);
    //注意这里引用也是有个很重要的细节的:我们不能直接传入m(u<<1)引用,因为我们虽然要实时更新但是不能改变u<<1的最值,所以设置一个中间变量作为传入参数
    int mx=m(u<<1),bt=b(u<<1);
    c(u)=c(u<<1)+cale(u<<1|1,mx,bt);
}
void build(int u,int l,int r)
{
    tr[u]={l,r,0,0,0,0};
    if(l==r)
    {
        tr[u]={l,r,a[l],0,b[l],0};
        return ;
    }
    int mid=l+r >> 1;
    build(u<<1,l,mid);
    build(u<<1|1,mid+1,r);
    pushup(u);
}
void modify_a(int u,int x,int y)
{
    if(tr[u].l==x&&tr[u].r==x)
    {
        m(u)=y;
        return ;
    }
    pushdown(u);
    int mid=tr[u].l+tr[u].r >> 1;
    if(x<=mid)modify_a(u<<1,x,y);
    if(x>mid)modify_a(u<<1|1,x,y);
    pushup(u);
}
void modify_b(int u,int l,int r)
{
    if(l<=tr[u].l&&r>=tr[u].r)
    {
        lz(u)^=1;b(u)^=1;
        return ;
    }
    pushdown(u);
    int mid=tr[u].l+tr[u].r >> 1;
    if(l<=mid)modify_b(u<<1,l,r);
    if(r>mid)modify_b(u<<1|1,l,r);
    pushup(u);
}
int query(int u,int x,int tp)
{
    if(tr[u].l==x&&tr[u].r==x)
    {
       if(!tp)return m(u);
       else return b(u);
    }
    pushdown(u);
    int mid=tr[u].l+tr[u].r >> 1;
    if(x<=mid)return query(u<<1,x,tp);
    if(x>mid)return query(u<<1|1,x,tp);
}
int query_ans(int u,int l,int r,int &mx,int &bt)
{
    if(l<=tr[u].l&&r>=tr[u].r)
    {
        if(mx<=m(u))return cale(u,mx,bt);
        else return 0;
    }
    pushdown(u);
    int cnt=0;
    int mid=tr[u].l+tr[u].r >> 1;
    if(l<=mid)cnt+=query_ans(u<<1,l,r,mx,bt);
    if(r>mid)cnt+=query_ans(u<<1|1,l,r,mx,bt);
    return cnt;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++)cin>>b[i];
    build(1,1,n);
    cin>>m;
    while(m--)
    {
        int op,x,y;
        cin>>op>>x>>y;
        if(op==1)modify_a(1,x,y);
        else if(op==2)modify_b(1,x,y);
        else
        {  
            int mx=query(1,x,0);
            int bt=query(1,x,1);
            cout<<query_ans(1,x,y,mx,bt)<<"\n";
        }
    }
    return 0;
}
  1. F 题 https://ac.nowcoder.com/acm/contest/11258/F 滑动窗口、线段树、dfs 序、主席树

大意:给定两棵以 1 为根的树,节点编号为 1-n。 现选择若干个点,构成一个新的集合s, s中的任意两点u,v 需满足u,v 在第一棵树中其中一个点是另一个点的祖先,在第二棵树满足u,v均不是对方的祖先。求满足条件的所选的点的最大数量。(1<=n<=3e5)

思路:官方解给的是主席树,滑动窗口+线段树的做法是对暴力查找每条链的优化,据说很容易能卡到 O ( n 2 l o g n ) O(n^2logn) O(n2logn), 但是数据卡的不严格,是能过的。感觉这种方法思路也是挺妙的,虽然官方解给的是主席树,但还是把这个方法写一下。

对于满足第一棵树上的若干个点,显然是在一条链上的点。对于满足第二棵树上的条件,显然是对于某个节点,以它为根的子树上所有的点都是不可取的。所以我们先在第二棵树上跑个dfs序,跑完之后,对于任意一个节点,以它为根的子树上所有的节点的dfs序都是比他大的且是连续的,我们在遍历第一棵树中的某条链时,若选了某个节点,以它为根的子树上的所有节点都是不能选的,所以我们可以把他们看成用过了。我们可以通过区间+1的方式,表示那些点选过了。一旦出现某个区间有值>=2,则表示选了两次,也就是构成了祖先关系,不合法。所以我们用线段树进行区间修改,并维护个最大值就可以很快的判定某条链合不合法了。然后我们在第一棵树上用滑动窗口遍历每条链就好了。树上滑动窗口太妙了,dfs yyds。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=300010;
int n,idx,ans;
vector<int> g1[N],g2[N];
int sfn[N],efn[N];
int q[N],hh=1,tt=0;
struct node{
    int l,r,mx,lazy;
}tr[N*4];
void pushup(int u)
{
    tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
void pushdown(int u)
{
    if(tr[u].lazy)
    {
        int t=tr[u].lazy;
        tr[u<<1].lazy+=t;tr[u<<1].mx+=t;
        tr[u<<1|1].lazy+=t;tr[u<<1|1].mx+=t;
        tr[u].lazy=0;
    }
}
void build(int u,int l,int r)
{
    tr[u]={l,r,0,0};
    if(l!=r)
    {
        int mid=l+r >> 1;
        build(u<<1,l,mid);build(u<<1|1,mid+1,r);
    }
}
void modify(int u,int l,int r,int x)
{
    if(l<=tr[u].l&&r>=tr[u].r)
    {
        tr[u].mx+=x;
        tr[u].lazy+=x;
        return ;
    }
    pushdown(u);
    int mid=tr[u].l+tr[u].r >> 1;
    if(l<=mid)modify(u<<1,l,r,x);
    if(r>mid)modify(u<<1|1,l,r,x);
    pushup(u);
}
int query(int u,int l,int r)
{
    if(l<=tr[u].l&&r>=tr[u].r)return tr[u].mx;
    pushdown(u);
    int mid=tr[u].l+tr[u].r >> 1;
    int mx=0;
    if(l<=mid)mx=query(u<<1,l,r);
    if(r>mid)mx=max(mx,query(u<<1|1,l,r));
    return mx;
}
void dfsord(int u,int fu)
{
    sfn[u]=++idx;
    for(auto &v:g2[u])
    {
        if(v==fu)continue;
        dfsord(v,u);
    }
    efn[u]=idx;
}
void dfs(int u,int fu)
{
    q[++tt]=u;
    modify(1,sfn[u],efn[u],1);
    int t=query(1,1,n);
    if(t>=2)//以该队头节点往下的链都是不行的,所以可以直接把队头弹出,相当于剪枝
    {
        modify(1,sfn[q[hh]],efn[q[hh]],-1);
        hh++;
    }
    ans=max(ans,tt-hh+1);
    for(auto &v:g1[u])
    {
        if(v==fu)continue;
        dfs(v,u);
    }
    //回溯
    modify(1,sfn[q[tt]],efn[q[tt]],-1);
    tt--;
    if(t>=2) //注意这里,因为上面是一旦出现>=2则表示含队头的往下的链一定是不行的把队头弹出了,但是回溯了之后相当于冲突的点去掉了,得把队头的重新加回来
    {
        hh--;
        modify(1,sfn[q[hh]],efn[q[hh]],1);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)
    {
        cin>>n;
        for(int i=1;i<n;i++)
        {
            int a,b;
            cin>>a>>b;
            g1[a].push_back(b);
            g1[b].push_back(a);
        }
        for(int i=1;i<n;i++)
        {
            int a,b;
            cin>>a>>b;
            g2[a].push_back(b);
            g2[b].push_back(a);
        }
        idx=0,ans=0,hh=1,tt=0;
        dfsord(1,0);
        build(1,1,n);
        dfs(1,0);
        cout<<ans<<"\n";
        for(int i=1;i<=n;i++)g1[i].clear(),g2[i].clear();
    }
    return 0;
}

下面是正解的做法:主席树+标记永久化

思路:我们在第二棵树跑完dfs 后也就转化成了区间问题,我们在第一棵树中跑每条链时,如果我们能很快的知道对于每个节点作为链底时它往上最近的没有冲突的节点的话,这样我们就可以只跑一遍dfs就行了。对于第二棵树中,祖先和子孙节点,很容易想到子孙节点是在祖先节点为根的子树上的,跑完 dfs 序转化成区间问题也就是子孙节点是祖先节点代表的区间的子区间。所以我们可以在第一棵树中 dfs 时,将每个节点对应的区间更新成深度,维护区间最大值,这样我们在对于每个链底查询对应区间的最大值就是以该点往上最近的有冲突的点。同时还得维护历史查询到的最大深度,例如在链 1-2-3-4-5-6-7 中,假设我们在 4 时查询到的最近的为 3 ,但是在 7 查询到的是 2 的话,以 7 为 链底的链因为有 4 存在 ,是不能用 2 去更新的,因为3 和 4是有冲突的。所以我们需要维护个历史最大深度。当然也可以直接用线段树,但是回溯操作很麻烦,所以我们直接用主席树写。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=300010;
struct node{
    int l,r,mx,lz;
}tr[N*40];
int idx,root[N],n,ans,dep[N];
int sfn[N],efn[N],tot;
vector<int> g1[N],g2[N];
void dfsord(int u,int fu)
{
    sfn[u]=++tot;
    for(auto &v:g2[u])
    {
        if(v==fu)continue;
        dfsord(v,u);
    }
    efn[u]=tot;
}
int insert(int p,int l,int r,int ql,int qr,int d)
{
    int q=++idx;
    tr[q]=tr[p],tr[q].mx=d;
    if(l>=ql&&r<=qr)
    {
        tr[q].lz=d;
        return q;
    }
    int mid=l+r >> 1;
    if(ql<=mid)tr[q].l=insert(tr[p].l,l,mid,ql,qr,d);
    if(qr>mid) tr[q].r=insert(tr[p].r,mid+1,r,ql,qr,d);
    return q;
}
int query(int p,int l,int r,int ql,int qr)
{
    if(ql<=l&&qr>=r)return tr[p].mx;
    int mid=l+r >> 1;
    int res=tr[p].lz;
    if(ql<=mid)res=max(res,query(tr[p].l,l,mid,ql,qr));
    if(qr>mid)res=max(res,query(tr[p].r,mid+1,r,ql,qr));
    return res;
}
void dfs(int u,int fa,int now)
{
    dep[u]=dep[fa]+1;
    int d=max(now,query(root[fa],1,n,sfn[u],efn[u]));
    ans=max(ans,dep[u]-d);
    root[u]=insert(root[fa],1,n,sfn[u],efn[u],dep[u]);//每次更新的必定比之前的大,直接更新就好了
    for(auto &v:g1[u])if(v!=fa)dfs(v,u,d);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)
    {
        cin>>n;
        for(int c=1;c<=2;c++)
            for(int i=1;i<n;i++)
            {
                int a,b;
                cin>>a>>b;
                if(c==1)g1[a].push_back(b),g1[b].push_back(a);
                else g2[a].push_back(b),g2[b].push_back(a);
            }
        dfsord(1,0);
        dfs(1,0,0);
        cout<<ans<<"\n";
        for(int i=1;i<=n;i++)g1[i].clear(),g2[i].clear();
        tot=idx=ans=0;
    }
    return 0;
}
  1. H 题 https://ac.nowcoder.com/acm/contest/11258/H 思维题

大意:给定一个长度为 n 序列 a ,问有多少个三元组 (i , j , k) 满足 a i ∗ a j = a k a_i*a_j=a_k aiaj=ak 。 (1<=n<=1e6,1<=a_i<=1e6)

思路:我们还是得从 a i ∗ a j = a k a_i*a_j=a_k aiaj=ak 我们一旦确定了 a i a_i ai ,然后就可以 a k a_k ak 一定是 a i a_i ai 的倍数,我们用桶把每个数出现的次数存下来,枚举 a i a_i ai

a i a_i ai 的倍数就能确定这样的三元组有多少个了,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
typedef long long LL;
int n,cnt[N],mx;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        cnt[x]++;
        mx=max(mx,x);
    }
    LL ans=0;
    for(int i=1;i<=mx;i++)
        for(int j=i;j<=mx;j+=i)
            ans+=(LL)cnt[i]*cnt[j/i]*cnt[j];

    cout<<ans<<"\n";
    return 0;
}
  1. I 题 https://ac.nowcoder.com/acm/contest/11258/I 签到
  2. J 题 https://ac.nowcoder.com/acm/contest/11258/J floyd、bitset 优化集合求交

大意:问floyd 求多源最短路时,把第一维的循环 k 放到第三维有多少个 dist[i,j] 仍是对的。并且认为 ∞=∞。n<=2000,m<=5000

思路:首先我们先来思考下 k 放到第三维的什么时候求的最短路仍是正确的。因为 floyd 是基于动态规划做的, k 的实际含义是状态定义f 数组的维数,k 放在第一层循环的时候我们用到在用到 k 的实际上要用上一层的 k,但是由于循环的特点,这一层循环的 k 是等于上一层的,所以我们可以第 k 这一维去掉,直接二维数组,三重循环更新就好了。但是一旦将 k 放到第三层就不行了。这时候再去掉 k 这维就换错误更新,对于 每个 u,v 最多用到一个正确 的 k 去求最短距离(有点抽象)。所以当满足以下条件时用错误的 floyd 算出的距离才是正确的。(1)u,v 有直接的边相连,且 dist[u,v] 就是 u 到 v 的最短路径。不需要 k 就是对的

​ (2)u,v 之间存在一点 z ,u,z是正确的,z,v 是正确的,并且 z 在 u,v 的一条最短路径上,这时候也能更新成正确的 dist[u,v]

接下来就是怎么来找出这样的点。根据n,m 的范围,我们可以预先通过对每个点跑一遍dijkstra, O ( n m l o g m ) O(nmlogm) O(nmlogm) 的时间复杂度求出任意两点的最短距离。这样对于第一种情况我们就可以很容易判定了。 对于第二种情况,先来想想怎么判定某个点在不在某两个点的最短路径上呢?我们可以预处理处两点间的最短路径都经过了哪些点。直接暴力做复杂度太高了,我们想想怎么优化,即尽量减少些重复操作。因为是最短路径嘛,我们从求最短路径的方法下手。对于某个起点 u, 我们在更新其他点到点 u 的最短路径时,是每次找到集合外距离 u 最近的点 v, 然后通过 v 点去更新其他的点t,因为我们之前已经求好任意两点的最短路径了,若 dist[u,v] + w[v,t]=dist[u,t] 则说明u,t 之间的最短路径是可以通过u,v] 去更新,所以u,t 之间的最短路径是在 u,v 的基础上又加了一个点,这样就避免了重复计算。

接下来就是我们怎么去判定第二种情况了。 我们纪录 从 u 出发到达哪些点是正确的,以及 从哪些点到 v 是正确的,这两个集合求交集就是同时满足两个条件的中转点,因为还得是最短路径上的点,所以继续和上面预处理出来的最短路径上的点求交集,如果求完交集之后集合不空的话就表明 u,v 之间是可以正确更新的。为了方便求交集我们直接用二进制代表集合,即用 bitset 。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=2010,inf=0x3f3f3f3f;
typedef pair<int,int> PII;
vector<PII> g[N];
int w[N][N],n,m,dist[N][N];
bitset<N> fr[N], to[N],pot[N];
void dijkstra(int s)
{
    for(int i=1;i<=n;i++)dist[s][i]=inf;
    priority_queue<PII  , vector<PII> ,greater<PII> > q;
    q.push({0,s});
    dist[s][s]=0;
    while(q.size())
    {
        auto t=q.top();
        q.pop();
        int u=t.second,d=t.first;
        for(auto &tt:g[u])
        {
            int v=tt.first,dd=tt.second;
            if(dist[s][v]>d+dd)
            {
                dist[s][v]=d+dd;
                q.push({dist[s][v],v});
            }
        }
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    memset(w,0x3f,sizeof w);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[a].push_back({b,c});
        w[a][b]=c;
    }
    for(int i=1;i<=n;i++)dijkstra(i);
    for(int i=1;i<=n;i++)w[i][i]=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(w[i][j]==dist[i][j])fr[i][j]=to[j][i]=1;

    vector<int> que(n);
    iota(que.begin(), que.end(), 1);
    for(int u=1;u<=n;u++)
    {
        sort(que.begin(),que.end(), [&](int x,int y){return dist[u][x]<dist[u][y];});
        for(int i=1;i<=n;i++)pot[i].reset(),pot[i][i]=1;
        for(auto &t:que)
            for(auto &v:g[t])
                if(dist[u][v.first]==dist[u][t]+v.second)pot[v.first]|=pot[t];
        for(int v=1;v<=n;v++)
        {
            auto tmp=fr[u]&to[v]&pot[v];
            if(tmp.count())
            {
                fr[u][v]=1;
                to[v][u]=1;
            }
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)ans+=fr[i].count();
    cout<<ans<<"\n";
    return 0;
}
  1. K 题 https://ac.nowcoder.com/acm/contest/11258/K 主席树,差分

大意:给定一个长度为 n 的序列 a , q次询问, f(l,r,k) 表示 把 a l , a l + 1 . . . . . . . a r a_l, a_{l+1}.......a_r al,al+1.......ar 每次进行区间+1或-1操作,求把 [l,r] 这段序列变成 a i a_i ai%k=0 的最小操作次数。n,q<=1e5 ,对于每次询问 0 < = a i < k 0<=a_i<k 0<=ai<k

思路:先不考虑 k,我们看下 将一个序列每次进行区间+1 或区间 -1 操作最少需要进行多少次操作。因为有区间+1、-1,对于这样的题目我们可以构建差分数组来做。 对于序列 a 1 a_1 a1 a n a_n an

我们构造出差分数组 b 1 = a 1 b_1=a_1 b1=a1 b n = a n − a n − 1 b_n=a_n-a_{n-1} bn=anan1 b n + 1 = − a n b_{n+1}=-a_n bn+1=an 这样就将问题转化成了每次选两个数,一个+1,一个-1,用最少的操作次数将它们全部变成0。为了尽快的变为0,必然是每次都选择有效操作,即每次选择一个>0的,一个<0的。因为 b 1 + . . . . . + b n + 1 b_1+.....+b_{n+1} b1+.....+bn+1 的总和为0,所以最终必然能全部变成0,这也是我们要弄到 b n + 1 b_{n+1} bn+1 的原因。最终操作次数 c n t = ∑ 1 n + 1 ∣ b i ∣ 2 cnt=\frac{\sum_1^{n+1}{|b_i|}}{2} cnt=21n+1bi

接下来考虑 k 的影响,我们最终要变成的 a i a_i ai%k=0。这就等价于我们可以先在区间进行若干次+k,-k不计入操作次数,最终还是可以转化成差分数组做,相当于我们可以先每次选几个数+k,-k。根据题目有 − k < b i < k -k<b_i<k k<bi<k

对于+k 来说,显然是选 b i < 0 b_i<0 bi<0 进行+k, 原本对结果的贡献是 ∣ b i ∣ |b_i| bi 后来变为 ∣ k + b i ∣ |k+b_i| k+bi 即 k- ∣ b i ∣ |b_i| bi 差值为s1= k − 2 ∣ b i ∣ k-2|b_i| k2bi

对于 -k 来说,显然是选 b i > 0 b_i>0 bi>0 进行 -k, 原本对结果的贡献是 ∣ b i ∣ |b_i| bi 后来变为 ∣ b i − k ∣ |b_i-k| bik 即 k- b i b_i bi 差值为s2= k − 2 ∣ b i ∣ k-2|b_i| k2bi

因为 − k < k − 2 ∣ b i ∣ < k -k<k-2|b_i|<k k<k2bi<k , 若选了一个 >0 的 + k ,或者 <0 的 -k 结果都会增加 k 一定不会使得结果变的更优,所以每次选的一定会是一个>0,一个<0 。这样就将问题变成了 s1+s2 最小。我们可以将所有<0的放到一块,所有>0的放到一块,然后变成全部变成 k − 2 ∣ b i ∣ k-2|b_i| k2bi

从小到大排序,可以求个前缀min就行了。但是这样复杂度太高,我们可以用主席树代替排序。但是求前缀min 复杂度还是高,仔细想想,前缀和是个单峰函数我们可以用二分或者三分写,(三分比二分慢了800ms左右,但是不用考虑边界问题更好些 )。这样我们就可以用 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n) 的时间复杂度解决了。注意头尾额外计算 a l a_l al a r a_r ar

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=200010,MX=(1<<30)-1;
int n,q,ql,qr,k;
LL pre[N],a[N];
struct Segtree{
	int rt[N],idx;
	struct node{
		int l,r,cnt;
		LL sum;
	}tr[N*50];
	void modify(int &rt,int l,int r,int x)
	{
		int now=++idx;
		tr[now]=tr[rt];
		tr[now].sum += x;
        tr[now].cnt += 1;
		if(l==r)
		{
			rt=now;
			return ;
		}
		int mid=l+r >> 1;
		if(x<=mid)modify(tr[now].l,l,mid,x);
		else modify(tr[now].r,mid+1,r,x);
		rt=now;
	}
	int getsz(int l,int r)
	{
		return tr[rt[r]].cnt-tr[rt[l]].cnt+1;// 头尾一正一负分别算一个
	}
	LL query(int l,int r,int ll,int rr,int kk,int x)
	{
		if(!kk)return 0;
		if(l==r)return (LL)l*kk;
		int mid=l+r >> 1;
		int cnt=tr[tr[rr].r].cnt-tr[tr[ll].r].cnt+(x>mid&&x<=r);
		if(cnt<=kk)return query(l,mid,tr[ll].l,tr[rr].l,kk-cnt,x)+tr[tr[rr].r].sum-tr[tr[ll].r].sum+x*(x>mid&&x<=r);
		else return query(mid+1,r,tr[ll].r,tr[rr].r,kk,x);
	}
}seg[2];
LL cale(int l,int r,int kk)
{
	LL res=(pre[r]-pre[l]+a[l]+a[r])/2;
	return res+(LL)k*kk-seg[1].query(0,MX,seg[1].rt[l],seg[1].rt[r],kk,a[l])-seg[0].query(0,MX,seg[0].rt[l],seg[0].rt[r],kk,a[r]);
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=2;i<=n;i++)
	{
		LL x=a[i]-a[i-1];
		pre[i]=pre[i-1]+abs(x);
		seg[1].rt[i]=seg[1].rt[i-1];
		seg[0].rt[i]=seg[0].rt[i-1];
		if(x>=0)seg[1].modify(seg[1].rt[i],0,MX,x);
		else seg[0].modify(seg[0].rt[i],0,MX,-x);
	}
	while(q--)
    {
		cin>>ql>>qr>>k;
		int l=0,r=min(seg[1].getsz(ql,qr),seg[0].getsz(ql,qr)),mid;
		while(l<=r)
		{
			mid=l+r >> 1;
			if(!mid||cale(ql,qr,mid)<cale(ql,qr,mid-1))l=mid+1;
			else r=mid-1;
		}
		cout<<cale(ql,qr,l-1)<<"\n"; //因为mid 更小时 l=mid+1, mid 出才是最优解
	}
	return 0;
}

总结:对于将一段区间全部变为0的题目,可以考虑用差分数组来做。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值