2024牛客寒假算法基础集训营2

目录

A.Tokitsukaze and Bracelet

B.Tokitsukaze and Cats

C.Tokitsukaze and Min-Max XOR

D.Tokitsukaze and Slash Draw

E and F.Tokitsukaze and Eliminate (easy)(hard)

G.Tokitsukaze and Power Battle (easy)

暂无

I.Tokitsukaze and Short Path (plus)

J.Tokitsukaze and Short Path (minus)

K.Tokitsukaze and Password (easy)


A.Tokitsukaze and Bracelet

阅读理解题,读懂题目按照要求模拟即可,重复的计算使用函数来解决

void solve(){
   
    int a,b,c; cin>>a>>b>>c;
    int ans=0;
    if(a==150) ans=1;
    if(a==200) ans=2;
    auto get = [&](int x){
        if(x>=29 && x<=32) return 0;
        if(x==45) return 2;
        return 1;
    };
    ans+=get(b)+get(c);
    cout<<ans<<endl;
    return ;
}

B.Tokitsukaze and Cats

我们要计算的是贡献,一个猫带来的贡献是看周围有没有其他的猫导致重复的所以我们就直接用

st[M][M]表示找个位置有没有猫然后看周围位置是不是有猫即可

bool st[M][M];
void solve(){
    cin>>n>>m>>k;
    int ans=0;
    while(k--){
        int a,b; cin>>a>>b;
        ans+=!st[a-1][b];
        ans+=!st[a+1][b];
        ans+=!st[a][b-1];
        ans+=!st[a][b+1];
        st[a][b]=true;
    }
    cout<<ans<<endl;
    return ;
}

C.Tokitsukaze and Min-Max XOR

1.记录方案数从数组中选一堆数出来的

2.与两个数异或<=k

接着分析我们可以发现选出这一堆数其实和整个数中有用的其实只有最大值和最小值,其他的都是看贡献即可

由此假设不考虑其他数的选择题目变成了选两个数异或小于等于k这也就是经典的trie树求解,但是考虑到树上的话,我们也是可以按照从小到大来排序不影响选择吗,同时保证到i的时候是最大值,那么贡献是什么呢贡献就是找到前面的一个满足的数后中间的数可选可不选2^{r-l-1}也就是\frac{2^{r-1}}{2^l}

也就是前面的数的贡献是后面的除以他那么我怎么记录前面的数的贡献呢?我们发现本题要的是逆元所以除以一个数可以变成乘以一个数的逆元由此 前缀可以用trie数+逆元的贡献来记录即可

然后就是普通的操作了

LL tr[N*32][2],cnt[N*32],idx;

LL qmi(LL a,LL b){
    LL res=1;
    while(b){
        if(b&1) res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}
LL inv(LL x){
    return qmi(x,mod-2);
}
void insert(int x,LL val){
    int p=0;
    for(int i=30;i>=0;i--){
        int u=x>>i&1;
        if(!tr[p][u]) tr[p][u]=++idx;
        p=tr[p][u];
        (cnt[p]+=val)%mod;
    }
}
LL query(int x,int y){
    int p=0;
    LL res=0;
    for(int i=30;i>=0;i--){
        int ux=x>>i&1,uy=y>>i&1;
        if(uy){
            res=(res+cnt[tr[p][ux]])%mod;
            if(!tr[p][!ux]) return res;
            p=tr[p][!ux];
        }
        else{
            if(!tr[p][ux]) return res;
            p=tr[p][ux];
        }
    }
    res=(res+cnt[p])%mod;
    return res;
}

void intn(){
    for(int i=0;i<=idx;i++)
        for(int j=0;j<=1;j++)
            tr[i][j]=0,cnt[i]=0;
    idx=0;
}
void solve(){
    intn();
    cin>>n>>k;
    vector<int> a(n+1);
    for(int i=1;i<=n;i++) cin>>a[i];
    sort(a.begin()+1,a.end());
    LL ans=0;
    for(int i=1;i<=n;i++){
        (ans+=query(a[i],k)*qmi(2,i-1)%mod+1)%=mod;
        insert(a[i],inv(qmi(2,i)));
    }
    cout<<ans<<endl;
   
    return ;
}

D.Tokitsukaze and Slash Draw

典型的轮换变化我们可以直接抽象为图论,由于是最优解也就是最快抵达这个点的解把数和数之间的转化直接看成边即可,然后用dijkstra,由于要他排在第k张牌也就是抽走上面的n-k张牌符合边的要求接着就是初始的时候是0用map存边的最小值减少方案注意LL

void solve(){
    
    cin>>n>>m>>k;
    unordered_map<int,int> mp;
    for(int i=1;i<=m;i++){
         int x,y; cin>>x>>y;
         x%=n;
         if(!mp.count(x)) mp[x]=y;
         else mp[x]=min(mp[x],y);
    }
    int need=n-k;
    auto dijkstra = [&](){
        vector<bool> st(n+5);
        for(int i=1;i<=n;i++) d[i]=2e18;
        priority_queue<PII,vector<PII>,greater<PII>> q;
        q.emplace(0,0);
        while(!q.empty()){
            auto [cost,u]=q.top(); q.pop();
            if(st[u]) continue;
            st[u]=true;
            if(u==need) return cost;
            for(auto&[v,w]:mp){
                int ne=(u+v)%n;
                if(d[ne]>cost+w){
                    d[ne]=cost+w;
                    q.emplace(d[ne],ne);
                }
            }
        }
        return (LL)-1;
    };
     
    cout<<dijkstra()<<endl;
    return ;
}

E and F.Tokitsukaze and Eliminate (easy)(hard)

做题的时候特别是计算贡献的时候一定要找到贡献的来源,我们可以发现消除一个数(如果找个数在右边第一次出现)之后其最右边的数都会消失,那么怎么做的贡献最少呢,当然十当前数组的每一个数都出现的时候最优的这样的删除一次删除的数最多可以(j结论明显所以直接用map存书的数量即可然后删除)这种最优性贡献都是如此思考

1.贡献来源

2.如何减少贡献

3.贡献是否最优

int a[N];
void solve(){
   
    map<int,int> mp,cnt;
    cin>>n;
    for(int i=1;i<=n;i++){
         cin>>a[i];
         cnt[a[i]]++;
    }
    int ans=0;
    for(int i=n;i>=1;i--){
        mp[a[i]]++;
        cnt[a[i]]--;
        if(mp.size()==cnt.size()){
            ans++;
            for(auto&[v,w]:mp){
                if(cnt[v]==0) cnt.erase(v);
            }
            mp.clear();
        }
    }
    cout<<ans<<endl;
    return ;
}

G.Tokitsukaze and Power Battle (easy)

我们来看看看题目的性质

1.每一个数都是大于等于0的数

2.我要的是[l,r]整个区间中选出一个子区间[ll,rr]中间找一个点[ll,x][x+1,rr]使得左边减去右边最小

做法一:线段树

由此我们可以发现左边的ll==l一定是最好的由条件1的不会变差,考虑右边那么一定是取一个数,

那么取哪一个数呢我们要求的就是sum[l,x]-a[x]\left (l<x<=r\right )变形之后sum_{y-1}-sum_{l-1}-a_y =sum_y-2*a_y-sum_{l-1} 也就是我们统一了下标得出维护的是区间中的sum_y-2*a_y,此时变成了区间维护整个值也就是区间修改需要懒标记维护同时维护前缀和,所以对于这个区间中除了y之外的数都是-change+before,对其单独处理即可

int w[N];
LL s[N];
struct code{
    int l,r;
    LL val,ans;
    LL tag;
}tr[4*N];
void pushup(code&u,code&l,code&r){
    u.val=l.val+r.val;
    u.ans=max(l.ans,r.ans);
}
void pushup(int u){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void pushdown(code&u,code&l,code&r){
    if(u.tag){
        l.ans+=u.tag,r.ans+=u.tag;
        l.tag+=u.tag,r.tag+=u.tag;
        u.tag=0;
    }
}
void pushdown(int u){
    pushdown(tr[u],tr[u<<1],tr[u<<1|1]);
}

void build(int u,int l,int r){
    if(l==r){
        tr[u]={l,l,w[l],s[l]-2*w[l]};
        return ;
    }
    tr[u]={l,r,0,(LL)-1e18};
    int mid=l+r>>1;
    build(u<<1,l,mid),build(u<<1|1,mid+1,r);
    pushup(u);
}

void modify(int u,int l,int r,int x,int op){
    if(l<= tr[u].l&& tr[u].r <=r){
       if(op==1)
           tr[u].ans+=x,tr[u].tag+=x;
       else 
           tr[u].val=x;
        return ;
    }
    pushdown(u);
    int mid=tr[u].l+tr[u].r>>1;
    if(l<=mid) modify(u<<1,l,r,x,op);
    if(r>mid) modify(u<<1|1,l,r,x,op);
    pushup(u);
}

LL query(int u,int l,int r,int op){
    if(l<=tr[u].l && tr[u].r<=r){
        return op==1 ? tr[u].val : tr[u].ans;
    }
    int mid=tr[u].l+tr[u].r>>1;
    LL res= op==1 ? 0 : -1e18;
    pushdown(u);
    if(r<=mid){
        if(op==1) res+=query(u<<1,l,r,op);
        else res=max(res,query(u<<1,l,r,op));
    }
    else if(l>mid){
        if(op==1) res+=query(u<<1|1,l,r,op);
        else res=max(res,query(u<<1|1,l,r,op));
    }
    else{
        if(op==1) res=res+query(u<<1,l,r,op)+query(u<<1|1,l,r,op);
        else res=max({res,query(u<<1,l,r,op),query(u<<1|1,l,r,op)});
    }
    return res;
}
void solve(){
   
    cin>>n>>m;
    for(int i=1;i<=n;i++){
         cin>>w[i];
         s[i]=s[i-1]+w[i];
    }
    
    build(1,1,n);
    
    while(m--){
        int op,l,r; cin>>op>>l>>r;
        if(op==1){
            modify(1,l,n,r-w[l],1);
            modify(1,l,l,-2*r+2*w[l],1);
            w[l]=r;
            modify(1,l,l,w[l],2);
        }
        else{
            LL res=query(1,l+1,r,2);
            if(l-1>=1) res-=query(1,1,l-1,1);
            cout<<res<<endl;
        }
    }
    return ;
}

做法二:树状数组

我们考虑对后缀进行移动最开始是 sum_r-sum_{l-1}-2*a_r

移动一位sum_r-sum_{l-1}-a_r-2*a_{r-1}

假设移动 一位地 贡献要大于上一位的话2*a_{r-1}<2*a_r 所以我们最多移动检查的位数就是32位这样可以直接用树状数组来维护

int w[N];
struct BIT{
    int tr[N];
    // int inline lowbit(int x){
        // return x&(-x);
    // }
    void add(int k,int x){
        for(int i=k;i<=n;i+=lowbit(i)) tr[i]+=x;
    }
    int query(int k){
        int res=0;
        for(int i=k;i;i-=lowbit(i)) res+=tr[i];
        return res;
    }
    int ask(int l,int r){// 先左再右
        return query(r)-query(l-1); 
    }
    void clear(){
        for(int i=0;i<=n+2;i++) tr[i]=0;
    }
}tree;

void solve(){
   
    
    cin>>n>>m;
    tree.clear();
    for(int i=1;i<=n;i++){
         cin>>w[i];
         tree.add(i,w[i]);
    }
    
    while(m--){
        int op,l,r; cin>>op>>l>>r;
        if(op==1){
            tree.add(l,r-w[l]);
            w[l]=r;
        }
        else{
            int ans=-1e18;
            for(int i=r-1;i>=max(r-32,l);i--){
                ans=max(ans,tree.ask(l, i)-w[i+1]);
            }
            cout<<ans<<endl;
        }
    }
    return ;
}

I.Tokitsukaze and Short Path (plus)

这是明显的计算贡献的题目我们看边的贡献是啥|a_v+a_u|+|a_v-a_u| = 2*max(a_u,a_v)

也就是说两个点之间的边贡献就是两个中的最大值的两倍,我们要计算的是\sum_i^n\sum_j^ndist_{i,j}

所以我们来看每一个点之间带来的贡献 如果直接抵达的话i和其他所有点的贡献是两者中的最大值

也就是一个点a和比他小的点的贡献就是dist_{a,i}=dist_{i,a}=a_i,同时简单思考两个点直接的最短路会不会中间有过度点,可以发现明显没有假设有的话可以简单论证一定比我现在找个要大,所以找个是最优的贡献,照权重由小到大排序之后sum=\sum_i^n2*a_i*(i-1)*2,注意开long long

void solve(){
   
    LL sum=0;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++){
        sum+=4*(LL)(i-1)*a[i];
    }
    cout<<sum<<endl;
    return ;
}

J.Tokitsukaze and Short Path (minus)

同上我们分析贡献dist_{u,v}=|a_u+a_v|-|a_u-a_v|=min(a_u,a_v)所以这次变成两个点中最小的了,但是如果直接同上一题是不是最优的呢?我们要考虑是否有中间点过度我们可以发现如果过度的是最小值a_1我们无法确定最优所以最优就是两者取最小值(也就是考虑齐全看我们的贡献是否是最优的) disti,j=min(2*a_1,min(a_i,a_j))

接着同上计算即可注意long long

int a[N];
void solve(){
   
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    sort(a+1,a+1+n);
    LL sum=0;
    for(int i=1;i<=n;i++){
        sum+=4ll*(n-i)*min(2*a[1],a[i]);
    }
    cout<<sum<<endl;
    return ;
}

K.Tokitsukaze and Password (easy)

首先我们读懂题目意思也就是我们需要的是 

1.没有前导0 

2.是8的倍数

3.<=y

4.其中的abcd字母是同字母数字相同不同字母数字不同(1-9),'-'只有一个也是1-9

数据范围1<=n<=9

首先我们可以发现整个数字的变化其实只有5^9所以我们可以考虑dfs直接暴力枚举所有情况即可

然后按照条件一个一个来

1. 特殊如果只有一个数可以为0,其他的如果有变化的话就是 从1开始,否则就是不满足要求

2&&3.由于数目较少到最后直接判断即可

4.(1)dfs枚举情况用map存储,同时st记录这个数是不是重复出现,同时记得还原现场

   (2)'-'直接枚举

   (3)枚举的同时需要满足没有前导0的要求

然后我们按照这个思考大纲书写代码即可

map<char,int> mp;
bool st[10];
void dfs(int u,LL res){
    if(u==n){
        if(res<=y && res%8==0) cnt++;
        return ;
    }
    if(s[u]>='0' && s[u]<='9'){
        if(s[u]=='0' && (!u && n!=1)) return ;
        dfs(u+1,res*10+(s[u]-'0'));
    }
    else if(s[u]=='_'){
        for(int i=(u==0 ? (n==1 ? 0 : 1) : 0);i<=9;i++){
            dfs(u+1,res*10+i);
        }
    }
    else{
        if(mp.count(s[u])) dfs(u+1,res*10+mp[s[u]]);
        else{
            for(int i=(u==0 ? (n==1 ? 0 : 1) : 0);i<=9;i++){
                if(st[i]) continue;
                mp[s[u]]=i;
                st[i]=true;
                dfs(u+1,res*10+i);
                st[i]=false;
                mp.erase(s[u]);
            }
        }
    }
}
void solve(){
    memset(st,0,sizeof st);
    mp.clear(); cnt=0;
    cin>>n>>s>>y;
    dfs(0,0);             
    cout<<cnt<<endl;
    return ;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值