2023牛客寒假算法基础集训营2个人题解

在csdn上写题解还会吞图和吞缩进,体验不太好,下次考虑换个地方发

AB.Tokitsukaze and a+b=n

题意

给定两个区间 [L_1,R_1],[L_2,R_2],在两个区间中分别找到a和b,使得 a+b=n,有多少种不同选法

思路

两区间找交集

由 a+b=n 可知 a = n-b,又知道 L_2≤b≤R_2,因此可得:

n-R_2≤a≤n-L_2.已知a的选择范围为:L_1≤a≤R_1,因此两区间求交集即为答案

我是对两个区间的所有交集情况进行了分类讨论,如果有更好的方法,欢迎在评论区讨论!

代码实现

 void solve()
 {
     int n=0;
     cin>>n;
     int l1=0,r1=0,l2=0,r2=0;
     cin>>l1>>r1>>l2>>r2;
     int l=n-r2,r=n-l2;
     intres=0;
     if(l1==r||l==r1) res=1;
     else if(r>=l1&&r<=r1&&l<=l1) res=r-l1+1;
     else if(r>=l1&&r<=r1&&l>=l1) res=r-l+1;
     else if(r>=r1&&l>=l1&&l<=r1) res=r1-l+1; 
     else if(r>=r1&&l<=l1) res=r1-l1+1;
     cout<<res<<endl;
     return;
 }

C.Tokitsukaze and a+b=n (hard)

题意

本题和AB题题意大致相同,不过本题中的a,b取值的两个区间并不指定,而是在给出的m个区间中任选两个,询问在这种情况下的选法。

思路

本题难以再用AB题中简单的两区间取交集来解决,但思想是一致的。

多个区间找交集,我们考虑对区间进行维护:

假设有一个全部为0的数轴,如果每被区间覆盖一次,就将该区间中的所有数+1, 那么最终询问 [L_i,R_i] 区间中所有数被覆盖的次数和,只需要对该区间进行求和即可。

本题解法就是应用了这种思路,对于每个区间找到与其他区间的交集。做法:枚举1~m个区间。假设枚举到第i个区间 [L_i,R_i] ,那么对于之后区间来说,第i个区间覆盖的范围是 [n-R_i,n-L_i],而对于第i个区间而言,求其与前面区间的交集即为对 [L_i,R_i] 区间进行求和。

对于区间修改与区间求和,我们这里用懒标记线段树实现。

有几个点要注意:

  • 规定的枚举顺序为单向 [i,j](1≤i≤j≤m),但 [j,i] 同时也是一种选法,因此结果要注意变为乘2

  • 本题的求和要不断mod p,防止整型溢出

  • 线段树维护的范围是从1开始的,不包括0,因此当 L_i≥n 时区间[n-R_i,n-L_i]不合法,直接跳过;当 R_i≥n 时,可以发现可能的使用区间不包括小于等于0的部分,因此直接将左端点n-r置为1即可。

具体实现看下方代码,若有不理解之处欢迎评论区提出或私聊。

代码实现

 int p=998244353;
 structnode {
     intl, r;
     intsum, add;//区间总和,懒标记
 }tr[N*4];
 ​
 voidpushup(intu)
 {
     (tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum)%=p;
 }
 ​
 voidpushdown(intu)
 {//下放懒标记,修改区间总和
     if(tr[u].add==0) return;
     node&rt=tr[u], &l=tr[u<<1], &r=tr[u<<1|1];
     l.add+=rt.add, r.add+=rt.add;
     (l.sum+= (l.r-l.l+1) *rt.add)%=p;
     (r.sum+= (r.r-r.l+1) *rt.add)%=p;
     rt.add=0;
 }
 ​
 voidbuild(intu, intl, intr)
 {
     if (l==r)
     {
         tr[u] = { l,r,a[l],0 };
         return;
     }
     else tr[u] = { l,r };
 ​
     int mid=l+r>>1;
     build(u<<1, l, mid), build(u<<1|1, mid+1, r);
     
     pushup(u);
 ​
     return;
 }
 ​
 intquery(intu,intl,intr)
 {
     if (tr[u].l>=l&&tr[u].r<=r) returntr[u].sum;
    
     pushdown(u);
 ​
     int sum=0;
     int mid=tr[u].l+tr[u].r>>1;
     if (l<=mid) (sum+=query(u<<1, l, r))%=p;
     if (r>mid) (sum+=query(u<<1|1, l, r))%=p;
 ​
     return sum;
 }
 ​
 voidmodify(intu, intl, intr, intd)
 {
     if (tr[u].l>=l&&tr[u].r<=r)
     {
         (tr[u].sum+= (tr[u].r-tr[u].l+1) *d)%=p;
         tr[u].add+=d;
     }
     else {
 ​
         pushdown(u);
 ​
         int mid=tr[u].l+tr[u].r>>1;
         if (l<=mid) modify(u<<1, l, r, d);
         if (r>mid) modify(u<<1|1, l, r, d);
 ​
         pushup(u);
     }
     return;
 } 
 ​
 voidsolve() {
     cin>>n>>m;
     build(1, 1, 4e5+10);
     int res=0;
     for(int i=1;i<=m;i++)
     {
         int l=0,r=0;
         cin>>l>>r;
         (res+=query(1,l,r))%=p;
         if (n-l<=0) continue;
         else if(n-r<=0) modify(1,1,n-l,1);
         else modify(1,n-r,n-l,1);
     }
     cout<<(2*res)%p<<endl;
 }

L.Tokitsukaze and Three Integers

题意

图被csdn吞了,建议直接看原题

思路

对于三个变化的变量,我们常常考虑去固定1~2个变量,而对其他变量进行单独处理。

我们在这里考虑对 a_k 进行固定,发现对于每个 x,可以通过 a_k 得到唯一的对应的 a_i*a_j 的值

因此我们首先预处理出 a_i*a_j 的值并记录其出现次数,然后枚举 a_k 通过x得到 a_i*a_j 值并找到其出现次数,出现多少次就有多少三元组 [i,j,k] 满足原式。

那么怎么保证 i≠j≠k 呢?

  • 在预处理 a_i*a_j 时指定 j>i 即可保证 i≠j

  • 在枚举a_k并用x得到指定值出现次数之前,将 a_k*a_i 的出现次数在总次数中减去,而在得到指定值出现次数之后再将其加回,即可保证 i≠k,j≠k

第二步是一种常用的临时处理重复的方法:先将重复的减去,待计算要求值完成后,再将重复的加上。

有几点需要注意:

  • 本题不要开long long,因为long long取模的时间大概是int的4倍

  • 因为本题不能开long long,所以一开始读入时要记得对 a_i 取模

  • 在去掉重复可能时,要将i=k的情况跳过,因为一开始计算 a_i*a_j 时i和j就不相同,此时再减去相当于多减了一次 i=j 的情况

  • 最终结果要乘2,原因同C题

补充:取模意义下的基本计算规则

  • 减法:先加p再减再模

  • 乘法:乘一次模一次;先模再乘再模 与 先乘再模 结果相同

代码实现

 void solve() {
     int n=0,p=0;
     cin>>n>>p;
     for(int i=1;i<=n;i++)
     {
         cin>>a[i];
         a[i] =a[i]%p;
     }
     for(int i=1;i<=n;i++)
     {
         for(int j=i+1;j<=n;j++)
         {
             int num=a[i]*a[j]%p;
             mp[num]++;
         }
     }
     for(int k=1;k<=n;k++)
     {
         for(int i=1;i<=n;i++)
         {
             if(i==k) continue;
             int num=a[i]*a[k]%p;
             mp[num]--;
         }
         for(int x=0;x<=p-1;x++)
         {
             int num= (x+p-a[k])%p;
             res[x]+=mp[num];
         }
         for(int i=1;i<=n;i++)
         {
             if(i==k) continue;
             int num=a[i]*a[k]%p;
             mp[num]++;
         }
     }
     for(int x=0;x<=p-1;x++)
         cout<<2*res[x]<<' ';
     return;
 } ​

J.Tokitsukaze and Sum of MxAb

题意

图被csdn吞了,建议直接看原题

思路

不难发现:max(|a_i-a_j|,|a_i+a_j|)=|a_i|+|a_j|.

由此可知,在关于i的所有(i,j)/(j,i) 对中,a_i 的贡献都是 |a_i|。

而 a_i 总共会被用到2*n次,因此a_i的总贡献就是2×n×|a_i|

代码实现

 void solve()
 {
     int n=0;
     cin>>n;
     int sum=0;
     for(int i=1;i<=n;i++)
     {
         cin>>a[i];
         sum+=abs(a[i]);
     }
     int res=2*n*sum;
     cout<<res<<endl;
     return;
 }

H.Tokitsukaze and K-Sequence

题意

将长度为n的序列a分成k个非空子序列(不一定连续),定义序列的值为这个序列中只出现一次的数字的个数。求当 k=1...n 时子序列值分别求和的最大值。

思路

依据定义,找到本题切入点为数字出现的次数,因此考虑当一个数字出现 i 次时的情况:

  • i≤k:i个数字可以分别分配到k个序列中,总贡献为i

  • i>k:k个数字分配到k-1个序列中,剩下所有数字分配到第k个序列中,总贡献为k-1

记录每种数字出现次数后,依据上述两种情况统计总贡献即可。

代码实现

 void solve() {
     int n=0;
     cin>>n;
     map<int,int>mp;
     for(int i=1;i<=n;i++)
     {
         cin>>a[i];
         tim[a[i]]++;
     }
     int cnt=0;
     for(int i=1;i<=1e5+10;i++){
         if(tim[i]!=0) t[++cnt] =tim[i];
     }
     sort(t+1,t+1+cnt);
     for(int i=1;i<=cnt;i++) s[i] =s[i-1]+t[i];
  
     for(int k=1;k<=n;k++)
     {
         int res=0;
         int l=0,r=cnt;
         while(l<r)//这里我用了二分,但实际上是不需要的,直接用指针找更简单
         {
             int mid=l+r+1>>1;
             if(t[mid]<=k) l=mid;
             else r=mid-1;
         }
         res+=s[l];
         res+=(cnt-l)*(k-1);
         cout<<res<<endl;
     }
     for(int i=1;i<=n;i++) tim[a[i]] =0;
     for(int i=1;i<=1e5+10;i++) t[i] =0;
     for(int i=1;i<=cnt;i++) s[i] =0;
     return;
 }

F.Tokitsukaze and Gold Coins (easy)

题意

有个n×3的迷宫,迷宫中有k个怪物。T从起点(1,1)出发,每次可以向下或者向右走,每走到一个新的格子就可以获得1个金币,并且途中不能走到怪物所在格子。当T走到终点后传送回起点,此时可以选择继续走到终点或者直接离开。询问最终T能获得的金币最大数量。

思路

本题本质上是求从起点到终点的所有路径上的所有点

经典做法:从起点与终点分别出发进行染色,最终染两种颜色的点即为要找的点。

不知道其他人怎么叫,但个人将其命名为:双端染色法

 int g[N][4];
 bool st1[N][4];
 bool st2[N][4];
 ​
 void solve() {
     int n=0,k=0;
     cin>>n>>k;
     for(int i=1;i<=k;i++)
     {
         int x=0,y=0;
         cin>>x>>y;
         if(!g[x][y]) g[x][y] =-1;
         else g[x][y] =0;
     }
 ​
     queue<pair<int,int>>q;
     q.push({1,1});
     int dx1[] ={0,1},dy1[] = {1,0};
     st1[1][1] =true;
     while(q.size())
     {
         auto t=q.front();
         q.pop();
 ​
         int x=t.first,y=t.second;
         for(inti=0;i<2;i++)
         {
             int cx=x+dx1[i],cy=y+dy1[i];
             if(st1[cx][cy]) continue;
             if(cx<1||cx>n||cy<1||cy>3) continue;
             if(g[cx][cy]==-1) continue;
 ​
             if(g[cx][cy]!=-1) g[cx][cy] =1;
             st1[cx][cy] =true;
             q.push({cx,cy});
         }
     }
 ​
     while(q.size()) q.pop();
 ​
     q.push({n,3});
     int dx2[] = {-1,0},dy2[] = {0,-1};
     st2[n][3] =true;
     while(q.size())
     {
         auto t=q.front();
         q.pop();
 ​
         int x=t.first,y=t.second;
         for(inti=0;i<2;i++)
         {
             int cx=x+dx2[i],cy=y+dy2[i];
             if(st2[cx][cy]) continue;
             if(cx<1||cx>n||cy<1||cy>3) continue;
             if(g[cx][cy]==-1) continue;
 ​
             if(g[cx][cy]==1) g[cx][cy] =2;
             st2[cx][cy] =true;
             q.push({cx,cy});
         }
     }
     g[1][1] =0;
     int res=0;
     if(g[n][3]==1) res++;
     for(int i=1;i<=n;i++)
     {
         for(int j=1;j<=3;j++)
         {
             if(g[i][j]==2) res++;
             g[i][j] =0;
             st1[i][j] =false,st2[i][j] =false;
         }
     }
     cout<<res<<endl;
 }

D.Tokitsukaze and Energy Tree

题意

将n个能量球放到一棵树n个节点上,并且在放入时可以得到以放入节点为根的子树中的所有能量球的能量(包括节点x)。

思路

可以发现,深度越大的节点可以提供的贡献越大,因此贪心地考虑将较大的能量球放在深度较深的节点,将较小的能量球放在深度较浅的节点即可。

代码实现

 void solve()
 {
     int n=0;
     cin>>n;
     for(int i=2;i<=n;i++)
     {
         int fa=0;
         cin>>fa;
         son[fa].push_back(i);
     }
     for(int i=1;i<=n;i++)
     {
         cin>>a[i];
     }
     
     sort(a+1,a+1+n);
 ​
     queue<PII>q;
     q.push({1,1});
     int res=0;
     int cnt=0;
     while(q.size())
     {
         auto t=q.front();
         q.pop();
         int ver=t.x,depth=t.y;
         res+=a[++cnt]*depth;
         
         for(auto s:son[ver])
         {
             q.push({s,depth+1});
         }
     }
     cout<<res<<endl;
     return;
 }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值