2021牛客暑期多校训练营4(字典树未补)

2021牛客暑期多校训练营4

导语

涉及的知识点

字符串、字典树、思维、数学、前缀和、逆序对

链接:2021牛客暑期多校训练营4

题目

C

题目大意:给出四个数a,b,c,n,构造三个字符串,构造三个小写字符串,使得 ∣ s 1 ∣ = ∣ s 2 ∣ = ∣ s 3 ∣ = n , L C S ( s 1 , s 2 ) = a , L C S ( s 2 , s 3 ) = b , L C S ( s 1 , s 3 ) = c |s_1|=|s_2|=|s_3|=n,LCS(s_1,s_2)=a,LCS(s_2,s_3)=b,LCS(s_1,s_3)=c s1=s2=s3=n,LCS(s1,s2)=a,LCS(s2,s3)=b,LCS(s1,s3)=c按照输入顺序输出字符串,不存在就输出NO

思路:折磨,折磨,还是折磨,写的时候字符串都构造出来了,结果在按照输入顺序输出上拌了脚。用贪心的思路,求得a,b,c的最小值,假设为a,那么s1,s2,s3这三个字符串设置都使用统一的a个相同字符填充,最后根据b-a,c-a去构造另外两对字符串即可,详见代码

代码

#include <bits/stdc++.h>

using namespace std;
int d[3],n,pos,m=1212,len1,len2,len3,mm;
bool vis[3];
char s[4][1212],ch='a';
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>d[0]>>d[1]>>d[2]>>n;
    for(int i=0; i<3; i++)
        if(m>=d[i]) {
            m=d[i];
            pos=i;
        }
    mm=m;
    for(int i=0; i<m; i++) {
        s[1][len1++]=ch;
        s[2][len2++]=ch;
        s[3][len3++]=ch;
    }
    ch++;
    m=1212;
    vis[pos]=1;
    for(int i=0; i<3; i++)
        if(m>=d[i]&&!vis[i]) {
            m=d[i];
            pos=i;
        }
    m-=mm;
    if(pos==0) {
        for(int i=0; i<m; i++) {
            s[1][len1++]=ch;
            s[2][len2++]=ch;
        }
    } else if(pos==1) {
        for(int i=0; i<m; i++) {
            s[2][len2++]=ch;
            s[3][len3++]=ch;
        }
    } else {
        for(int i=0; i<m; i++) {
            s[1][len1++]=ch;
            s[3][len3++]=ch;
        }
    }
    ch++;
    m=1212;
    vis[pos]=1;
    for(int i=0; i<3; i++)
        if(m>=d[i]&&!vis[i]) {
            m=d[i];
            pos=i;
        }
    m-=mm;
    if(pos==0) {
        for(int i=0; i<m; i++) {
            s[1][len1++]=ch;
            s[2][len2++]=ch;
        }
    } else if(pos==1) {
        for(int i=0; i<m; i++) {
            s[2][len2++]=ch;
            s[3][len3++]=ch;
        }
    } else {
        for(int i=0; i<m; i++) {
            s[1][len1++]=ch;
            s[3][len3++]=ch;
        }
    }
    ch++;
    if(len1>n||len2>n||len3>n)
        cout <<"NO"<<endl;
    else {
        for(int i=len1; i<n; i++)
            s[1][i]=ch;
        ch++;
        for(int i=len2; i<n; i++)
            s[2][i]=ch;
        ch++;
        for(int i=len3; i<n; i++)
            s[3][i]=ch;
        for(int i=1; i<=3; i++)
            cout <<s[i]<<endl;
    }

    return 0;
}

E

题目大意:给出具有n个节点的树与每个节点的取值范围 [ l i , r i ] [l_i,r_i] [li,ri],给出每个边权的值,边权值为端点间异或,询问有多少种节点赋值方案能满足给定条件

思路:首先节点的赋值方案只与根节点有关,假设根节点权值为 w 0 = a w_0=a w0=a,与根节点相邻的节点权值便可以确定了 w i = e i ⊕ a w_i=e_{i}\oplus a wi=eia,同理,其他点也可以确定下来,并且各点的权值表达式为 w t = e i ⊕ e i + 1 ⊕ ⋯ ⊕ a w_t=e_{i}\oplus e_{i+1}\oplus \dots \oplus a wt=eiei+1a,可见都有a,因此对应给定的约束条件就变为了 l i ≤ e i ⊕ a ≤ r i l_i\le e_i\oplus a\le r_i lieiari,于是问题转换为求满足各个点的约束条件的a有多少个,a的范围为 [ l i ⊕ e i , r ⊕ e i ] [l_i \oplus e_i,r\oplus e_i] [liei,rei],但是异或之后,所得的不一定为连续的范围,需要把它分成多个连续的区间

获得多个取值区间后,需要求得这些区间的交集,交集中的元素个数即为方案个数,基本的思想就是记录每个点被覆盖的次数(差分),找到覆盖了n次的点有几个即可,但是由于区间不连续,所以不能直接暴力

对于整个不连续的区间,需要将他们切割成连续区间,首先可以将 [ l i , r i ] [l_i,r_i] [li,ri]用二进制分割成多个区间长度为2的整数次幂的区间,分割之后的区间有一个性质,假设分割后的区间为 [ x , x + 2 t − 1 ] [x,x+2^t-1] [x,x+2t1],该区间内每个数的前k位都是相同的(如图,用二进制的原理也很容易想到),对该区间异或操作分为前k位和后n-k位来讨论,对前k位异或同一值显然不变,对后n-k位异或后得到的区间仍然为自身,以取模的思想来理解,这样 [ l i , r i ] [l_i,r_i] [li,ri]被分成了多个异或之后仍然连续的区间。

根据数据范围,可以用230的总长度去构造每个 [ l i , r i ] [l_i,r_i] [li,ri]的切割区间,对于这些区间进行+1(差分),累计被覆盖次数为n的点即可,由于数据范围过大,只能把差分的首和尾用结构体存储并排序,计算时遍历结构体更新答案
在这里插入图片描述
关于operation函数

这个函数是实现切割的关键部分,首先明确一点,在函数中对给定区间异或实质上为对区间各个数的前k位异或,因为对剩下的位异或之后组成的仍然为区间。len用来获取区间长度,a1用来获取区间异或之后的左端点,len-1获取的是后n(n只是l的总位数)-k位全1时的值,~(len-1)将这些位边为0,val&( ~(len-1) )将异或值的n-k为置0,因为这些位异或后区间仍然连续,不会修改区间的大小与位置,l^(val&( ~(len-1) ))最后获得执行异或操作之后的l (val的前k位与l异或),由于区间长度不变,l+len便可以得到r

代码

#include <bits/stdc++.h>

using namespace std;
const int maxn=1e5+5;
int n,l[maxn],r[maxn],head[maxn],tot,W[maxn],ans,sum;
struct node {
    int next,to,w;
} e[2*maxn];//链式前向星
struct Seg {
    int val,lazy;
    bool operator<(const Seg&t)const {
        if(val!=t.val)
            return val<t.val;
        return lazy<t.lazy;
    }
};
vector<Seg>V;
void Add(int from,int to,int w) {
    e[++tot].to=to;
    e[tot].next=head[from];
    e[tot].w=w;
    head[from]=tot;
}
void DFS(int u,int fa,int val) {
    W[u]=val;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to,p=e[i].w;
        if(v!=fa)
            DFS(v,u,val^p);
    }
}
void operation(int l,int r,int val) {
    Seg a1,a2;
    int len=r-l+1;//区间长度
    a1.val=(l^(val&(~(len-1))));//确认左端点
    a2.val=(l^(val&(~(len-1))))+len;//获取右端点
    a1.lazy=1,a2.lazy=-1;
    V.push_back(a1);
    V.push_back(a2);
}
void BinarySearch(int L,int R,int l,int r,int val) {
//把区间二分到完美线段树的具体位置上,即一个个完整的二进制区间
    if(L<=l&&R>=r) {//找到对应区间在哪
        operation(l,r,val);//被覆盖,差分操作
        return ;
    }
    int mid=(l+r)>>1;
    if(L<=mid)
        BinarySearch(L,R,l,mid,val);
    if(R>mid)
        BinarySearch(L,R,mid+1,r,val);
}
int main() {
    ios ::sync_with_stdio(0),cin.tie(0);
    cin >>n;
    for(int i=1; i<=n; i++)//存范围
        cin >>l[i]>>r[i];
    for(int i=1; i<=n-1; i++) {//录入边
        int u,v,w;
        cin >>u>>v>>w;
        Add(u,v,w);
        Add(v,u,w);
    }
    DFS(1,0,0);//构造初始树,假设根节点值为0
    for(int i=1; i<=n; i++)
        BinarySearch(l[i],r[i],0,(1<<30)-1,W[i]);//对0~(1<<30)-1模拟线段树操作
    sort(V.begin(),V.end());//对差分用的结构体排序
    int len=V.size();
    for(int i=0; i<len-1; i++) {
        sum+=V[i].lazy;
        if(sum==n)//该点到下一点前面的区间覆盖了n次,说明满足了n个异或不等式
            ans+=V[i+1].val-V[i].val;
    }
    cout <<ans;
    return 0;
}

本题还有字典树写法,链接在文章末,之后有时间会再加上

F

题目大意:一个无向图,A和B轮流操作,要么拿走一条边,要么拿走一个无环的连通分量,谁最后拿不了判负,假设两者都执行最优策略,判断谁赢

思路:开始把题目想复杂了,对于第一种操作, 会使得边数 -1,对于第二种操作, 会使得点数 -k, 边数 -(k-1),最后答案只和(n+m)奇偶性有关

代码

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n,m;
    cin >>n>>m;
    int a,b;
    for(int i=0;i<m;i++)
        cin >>a>>b;
    if((n+m)%2==1)
        cout <<"Alice"<<endl;
    else
        cout <<"Bob"<<endl;
    return 0;
}

I

题目大意:给出一个1~n的排列,定义序列权重为逆序对个数,现在能对每个位置的值+1或者不变,求出经过最优策略后能获得的权重最小值

思路:队友写的,逆序对的部分本来可以用树状数组,但是我当时在做字符串…队友找了合并排序的模板花费了些时间解出来了,以后有数据结构的简化部分不能放过

用贪心的思想去构造,对于x,如果x-1的位置在x之后,增大x-1对应位置上的值,即构造出一对x,x,标记x-1已经使用,继续遍历完,对遍历完的数据统计逆序对即可

代码

#include <bits/stdc++.h>

using namespace std;
const int maxn=2e5+10;
int n,pos[maxn],vis[maxn],a[maxn],tree[4*maxn];
long long ans;//注意long long
void update(int x) {
    for(; x<=n; x+=-x&x)
        tree[x]++;
}
int sum(int x) {
    int acc=0;
    for(; x; x-=x&-x)
        acc+=tree[x];
    return acc;
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    cin >>n;
    for(int i=1; i<=n; i++) {
        cin >>a[i];
        pos[a[i]]=i;
    }
    for(int i=2; i<=n; i++)
        if(pos[i-1]>pos[i]&&!vis[i-1]) {
            a[pos[i-1]]++;
            vis[i]=1;
        }
    for(int i=1; i<=n; i++) {//统计逆序对
        ans+=i-1-sum(a[i]);
        update(a[i]);
    }
    cout <<ans;
    return 0;
}

逆十字的代码
先树状数组,再减去减少的逆序对

#include <bits/stdc++.h>

using namespace std;
const int maxn=2e5+5;
int n,a[maxn],t[4*maxn],p[maxn];
long long ans;//开long long 统计
void update(int x) {
    for(; x<=n; x+=x&-x)
        t[x]++;
}
int query(int x) {
    int res=0;
    for(; x; x-=x&-x)
        res+=t[x];
    return res;
}
int main() {
    ios ::sync_with_stdio(0),cin.tie(0);
    cin >>n;
    for(int i=1; i<=n; i++) {
        cin >>a[i];
        p[a[i]]=i;
    }
    for(int i=1; i<=n; i++) {//获得原始的逆序对个数
        ans+=i-1-query(a[i]);//i-1是因为以已经使用过的数为基准,判断在a[i]之前的个数
        update(a[i]);
    }
    bool flag=0;
    for(int i=2; i<=n; i++) {
        if(p[i]>p[i-1])//满足条件略过
            flag=0;//更新标记
        else if(flag)//代表这个位置已经使用过
            flag=0;
        else
            ans--,flag=1;//减少一个相邻的对
    }
    cout <<ans;
    return 0;
}

J

题目大意:n×m的矩阵W,给出两个序列 a 1 … n , b 1 … m , W i , j = a i + b j a_{1\dots n},b_{1\dots m},W_{i,j}=a_i+b_j a1n,b1m,Wi,j=ai+bj,现在求出一个拥有最大平均值的子矩阵,给出子矩阵的高和宽至少为多少,求出最大平均值的子矩阵的平均值

思路:分成横纵两个部分来处理,分别获取两部分各自符合条件的最大平均值,累加。使用二分来探索平均值,即每次以上界+下界的一半判断能否作为平均值,其他详见代码

关于judge函数

在judge函数中,构造了b数组存储原值与平均值的差值,并构造b数组的前缀和,对于一个连续的b的区间和sum,如果sum<0,代表给定平均值无法作为该区间的平均值,如果sum>=0,代表该区间平均值≥给定平均值,在judge函数的后半段中,为了确保区间长度,sum[i]中i必须从给定长度f开始取,所求得的sum值,实质上为sum[i]-sum[x],在代码中,所得的minn一定≤0,因为sum[0]为0,因此,minn包含的区间只会使得平均值减小,需要减去,逐步扩大i的取值,每次求得减去之后的最值即可
在这里插入图片描述

代码

#include <bits/stdc++.h>

using namespace std;
const int maxn=1e5+10;
int n,m,x,y,a[maxn];//a存数据
double res,b[maxn],sum[maxn];//b为a数组减去假设平均值
double judge(int n,double mid,int f) {//判断是否可行
    for(int i=1; i<=n; i++) {
        b[i]=a[i]-mid;//获得每一项与平均值的差值
        sum[i]=sum[i-1]+b[i];//累和差值,如果最终差值为负数,代表此平均值不行
    }
    double minn=1e10,maxx=-1e10;
    for(int i=f; i<=n; i++) {//保证高度
        minn=min(minn,sum[i-f]);//获得值最小的从初始开始的区间
        maxx=max(maxx,sum[i]-minn);//sum[i]累和到当前位置的累和,判断去掉前置区间的最值
    }
    //类似尺取法
    return maxx;
}
double largestAve(int n,int f) {
    double mid,l=0,r=1e5,eps=1e-8;
    while(r-l>eps) {//二分平均值,尝试能否以该值为平均值
        mid=(r+l)/2;
        if(judge(n,mid,f)>0)//如果获得平均值可用
            l=mid;
        else
            r=mid;
    }
    return r;
}
int main() {

    cin >>n>>m>>x>>y;
    for(int i=1; i<=n; i++)
        cin >>a[i];
    res+=largestAve(n,x);//获取纵列最大平均值
    for(int i=1; i<=m; i++)
        cin >>a[i];
    res+=largestAve(m,y);//获取横排最大平均值
    printf("%.10lf",res);
    return 0;
}

参考文献

  1. 2021牛客暑期多校训练营4 E. Tree Xor(线段树/Trie)超详细题解
  2. 2021牛客暑期多校训练营4 E - Tree Xor(合法异或区间)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值