2022牛客寒假算法基础集训营1 - 题解

A . 九小时九个人九扇门

算法分析

dp 0/1 背包 升级版

我们可以注意到一点就是 : 一个数的数字根等于这个数对 9 取模的结果(特别地,取模得 0则数字根为9)

(abcd)%9 = a*1000%9+b*100%9+c*10%9+d%9

                  =a%9+b%9+c%9+d%9

                  =(a + b + c + d) % 9 

(abcd)%9 为这个数模 9 的结果

(a+b+c+d) 为数字根 证毕!!!

利用这个结论我们就可以方便对问题的求解了

状态表示

f[i][j] 表示从前 i 个数中取得数之和对 9 取模为 j 的方案数

状态转移

很简单就能想到就是第 i 个数取或者不取这两种情况

1. 取的情况f[i](j + a[i]) % 9] = (f[i](j + a[i]) % 9] + f[i - 1][j]) % mod

2. 不取的情况 f[i][j] = f[i][j] + f[i - 1][j] % mod

答案表示

用 f[n][i] 表示即可,特别的需要注意九号门的答案是f[n][0] - 1,(因为 9%9=0 初始状态不取的时候取模也是 0 此处多取了一种方案,将其去除 )

AC code

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

const int mod = 998244353;
LL f[100000 + 10][10];
LL a[100000 + 10];

int main()
{
    int n;
    cin >> n;
    for(int i = 1;i <= n;i ++)
    {
        int x;scanf("%d",&x);
        x %= 9;
        a[i] = x;
    }
    f[0][0] = 1;
    //f[i][j] : 表示前 i 个数求和 % 9 后得 j 的方案数
    for(int i = 1;i <= n;i ++)
     for(int j = 0;j <= 8;j ++)
      {
          f[i][(j + a[i]) % 9] = (f[i][(j + a[i]) % 9] + f[i - 1][j]) % mod;
          f[i][j] = (f[i][j] + f[i - 1][j]) % mod;
      }
    
    for(int i = 1;i <= 8;i ++)
    printf("%d ",f[n][i]);
    printf("%d",f[n][0] - 1);
    return 0;
}

B.炸鸡块君与FIFA22

算法分析

ST表 (倍增),概念还是很好理解的,有必要掌握一下

起始分数在 %3 的意义下相同,若 %3 后的数是一样的,那么在经历了同一段的区间上的贡献是一样的,因此这是一个存在重复贡献的问题,可以用 ST 表解决。

用 f[k][i][j]  表示初始分数 %3 为 k,从 i 号位置开始,长度为(1 << j)的这段区间上的贡献。

 那么就是经典的倍增过程,本题就需要多注意求完上一个一半的区间后的,下一个区间的初始分数会发生变化的,即下列式子的后半段

f[k][i][j] = f[k][i][j-1]+f[(k + f[k][i][j] + 3) %3][j-1]

void init()
{
    for(int j = 0;j < M;j ++)
     for(int i = 1;i + (1 << j) - 1 <= n;i ++)
      for(int k = 0;k < 3;k ++)
       if(!j)//初始化
       {
           if(s[i] == 'W') f[k][i][j] = 1;
           else if(s[i] == 'L') 
           {
               if(k != 0) f[k][i][j] = -1;
           }
       }
       else 
       {
           f[k][i][j] = f[k][i][j - 1] + f[(k + f[k][i][j - 1] + 3)% 3][i + (1 << (j - 1))][j - 1];
       }
}

询问的话,也是老的套路,在这个区间上倍增,能取到的把他加上就完事了。

pos 维护当前的位置,然后依靠 j 不断倍增,然后注意跳到每个位置后分数会改变就行

int query(int l,int r,int score)
{
    int pos = l;
    while(pos <= r)
    {
        int j = 0;
        while(pos + (1 << j) - 1 <= r) j ++;
        j --;
        score += f[(score + 3) % 3][pos][j];
        pos += (1 << j);
    }
    return score; 
}

AC code

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;
int n,q;
const int N = 2e5 + 10,M = 20;

char s[N];
int f[3][N][M];//f[k][i][j]:分数 % 3 为 k,在以 i 为起始位置,长度为 (1 << j) 的区间上的得分情况

void init()
{
    for(int j = 0;j < M;j ++)
     for(int i = 1;i + (1 << j) - 1 <= n;i ++)
      for(int k = 0;k < 3;k ++)
       if(!j)//初始化
       {
           if(s[i] == 'W') f[k][i][j] = 1;
           else if(s[i] == 'L') 
           {
               if(k != 0) f[k][i][j] = -1;
           }
       }
       else 
       {
           f[k][i][j] = f[k][i][j - 1] + f[(k + f[k][i][j - 1] + 3)% 3][i + (1 << (j - 1))][j - 1];
       }
}

int query(int l,int r,int score)
{
    int pos = l;
    while(pos <= r)
    {
        int j = 0;
        while(pos + (1 << j) - 1 <= r) j ++;
        j --;
        score += f[(score + 3) % 3][pos][j];
        pos += (1 << j);
    }
    return score; 
}
int main()
{
    cin >> n >> q;
    scanf("%s",s + 1);
    init();
    while(q --)
    {
        int l,r,sc;
        cin >> l >> r >> sc;
        printf("%d\n",query(l,r,sc));
    }
    return 0;
}

F.中位数切分

算法分析

        一个区间内的中位数能够大于等于 m 说明,这个区间内大于等于 m 的数的个数(在这里记作cnt1) 一定比小于 m 的数的个数(在这里记作 (cnt2) 多 。那么也就是说只要cnt1- cnt2 >= 1 就是可以满足条件的。那么为了尽可能多的划分区间,肯定是希望刚好为 1 的时候就将其划分。那么每一个区间将会消耗一份,那么最终划分的区间总数就会是cnt1 - cnt2      

AC code

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
void solve()
{
    int n,m;
    cin >> n >> m;
    int cnt1 = 0,cnt2 = 0;
    for(int i = 1;i <= n;i ++)
    {
        int x;
        cin >> x;
        if(x >= m) cnt1 ++;
        else cnt2 ++;
    }
    if(cnt1 - cnt2 >= 1) printf("%d\n",cnt1 - cnt2);
    else puts("-1");
}
int main()
{
    int T;
    cin >> T;
    while(T --)
    solve();
    
    return 0;
}

G.ACM is all you need

算法分析

我们可以注意到这个函数,|f(x)-b|+b 只有在 f(x)<b 的时候变换成2b-f(x)才会对答案有影响,因此我们只需要讨论 f(x) 的值,和 b 的取值即可。

1. a[i] < a[i-1],a[i]<a[i + 1]

2.a[i] > a[i - 1],a[i] > a[i + 1]

3.a[i] > a[i - 1],a[i + 1] > a[i]

4.a[i]<a[i-1],a[i]>a[i + 1]

讨论 b 的取值在何时才会改变当前状况,对答案产生贡献

具体的讨论公式已经呈现在代码的注释中。

最后取答案,就是在区间上取答案的办法,遇到左右端点加减贡献即可

AC code

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;


void solve()
{
    int a[N];
    map<int,int> mp;//存不同的 b 值可以使原函数去除多少 local minimum
    int n;
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i];
    int res = 0;//原本存在多少个 local minimum
    for(int i = 2;i <= n - 1;i ++)
    {
        if(a[i] < a[i - 1] && a[i] < a[i + 1])
        {
            res ++;
            int x = min(a[i - 1],a[i + 1]);
            // 2b - a[i] >= x    b >= (a[i] + x) /2  那么以(a[i] + x) / 2 为左端点 以后的区间 贡献 + 1
            mp[(a[i] + x + 1)/2] ++;
        }
        else if(a[i] > a[i - 1] && a[i] > a[i + 1])
        {
            int x = max(a[i - 1],a[i + 1]);
            //  |x - b| > |a[i] - b| => b > (a[i] + x) /2
            // 以 b 为左端点的贡献都要减 1
            mp[(a[i] + x)/2 + 1] --;
        }
        else if(a[i] > a[i - 1] && a[i] < a[i + 1])
        {
            // |b - a[i - 1]| > |a[i] - b| 时会构成局部最小 左端点开始 贡献 --
            mp[(a[i] + a[i - 1])/2 + 1] --;
            // |b - a[i]| > |a[i + 1] - b| 时不再构成局部最小 右端点结尾 贡献 ++
            mp[(a[i] + a[i + 1] + 1) / 2] ++;
        }
        else if(a[i] < a[i - 1] && a[i] > a[i + 1])
        {
            // |b - a[i + 1]| > |a[i] - b| 时会构成局部最小 左端点开始 贡献 --
            mp[(a[i] + a[i + 1])/2 + 1] --;
            // |b - a[i]| > |a[i - 1] - b| 时不再构成局部最小 右端点结尾 贡献 ++
            mp[(a[i] + a[i - 1] + 1)/2] ++;
        }
    }
    int ans = 0,tmp = 0;
    for(auto it : mp)
    {
        tmp += it.second;
        ans = max(ans,tmp);
    }
    printf("%d\n",res - ans);
    return ;
}

int main()
{
    int T;
    cin >> T;
    while(T --)
    solve();
    return 0;
}



H.牛牛看云

算法分析

n 的数据范围限制了我们暴力求解的做法,同时我们也注意到了0<=ai<=1000,这提示了我们在值域上进行暴力就可以了,暴力枚举0-1000上所有数字,两两搭配,

枚举到不同的数字 i,j ,那么同样的数值就要计算,num[i]*num[j]

枚举到不同的数字 i,j,那么同样的数值就要计算,_{num[i]}^{2}\textrm{C} + num[i],因为_{num[i]}^{2}\textrm{C}只考虑了取两个不同的情况,但是本题自身是允许被取到的因此再补上 num[i] 即可。

 

AC code

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;

LL a[N],num[1005];

int main()
{
    int n;
    cin >> n;
    for(int i = 1;i <= n;i ++) cin >> a[i],num[a[i]] ++;
    LL ans = 0;
    for(int i = 0;i <= 1000;i ++)
     for(int j = i;j <= 1000;j ++)
      {
          LL res;
          if(i == j) res = num[i] + (num[i] * (num[i] - 1) / 2);
          else res = num[i] * num[j];
          ans = ans + res * (LL)abs(i + j - 1000);
      }
    printf("%lld\n",ans);
    return 0;
}

K.冒险公社

算法分析

一个简单但是比较麻烦的线性dp

状态表示

用 dp[i][j][k][l] 表示在前 i 个岛屿中,第 i 个岛屿颜色为 j,第 i-1 个岛屿颜色为 k,第 i-2 个岛屿颜色为 l 时,最多有多少个绿岛

状态转移

本文用的是,Red:1    Green:2   Black:3 

dp[i][j][k][l] 肯定是从 dp[i - 1][j][k][?] 转移过来的

 for(int now = 4;now <= n;now ++)
     for(int i = 1;i <= 3;i ++)//假设出i,j,k三个岛颜色的所有情况
      for(int j = 1;j <= 3;j ++)
       for(int k = 1;k <= 3;k ++)
       {
           int r = 0,g = 0,b = 0;//统计红绿黑颜色情况
           if(i == 1) r ++;if(i == 2) g ++;if(i == 3) b ++;
           if(j == 1) r ++;if(j == 2) g ++;if(j == 3) b ++;
           if(k == 1) r ++;if(k == 2) g ++;if(k == 3) b ++;
           
           for(int l = 1;l <= 3;l ++)
           {
               if(dp[now - 1][j][k][l] == - 1) continue;//不存在合法方案直接跳过
               if(g > r && s[now] == 'G') dp[now][i][j][k] = max(dp[now][i][j][k],dp[now - 1][j][k][l] + (i == 2));
               if(g == r && s[now] == 'B') dp[now][i][j][k] = max(dp[now][i][j][k],dp[now - 1][j][k][l] + (i == 2));
               if(g < r && s[now] == 'R') dp[now][i][j][k] = max(dp[now][i][j][k],dp[now - 1][j][k][l] + (i == 2)); 
           }
       }

now 表示当前枚举到了第 now 个岛屿,考虑以 now 结尾的三个岛屿的情况和以now-1 结尾的三个岛屿的情况,暴力判断其是否满足条件即可

限制条件就是

1. now 岛是绿色的时,以now 为结尾的三个岛屿中,绿色的岛屿数量一定要大于红色岛屿数量

2. now 岛是红色的时,以now 为结尾的三个岛屿中,红色的岛屿数量一定要大于绿色岛屿数量

3. now 岛是黑色的时,以now 为结尾的三个岛屿中,绿色的岛屿数量一定要等于红色岛屿数量

 

AC code

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int dp[N][4][4][4];
char s[N];
// R 1      G 2         B 3
int main()
{
    
    int n;
    cin >> n;
    scanf("%s",s+1);
    memset(dp,-1,sizeof dp);
    
    //初始化前三个的岛的状态
    for(int i = 1;i <= 3;i ++)
     for(int j = 1;j <= 3;j ++)
      for(int k = 1;k <= 3;k ++)
      {
          int r = 0,g = 0,b = 0;
          if(i == 1) r ++;if(i == 2) g ++;if(i == 3) b ++;
          if(j == 1) r ++;if(j == 2) g ++;if(j == 3) b ++;
          if(k == 1) r ++;if(k == 2) g ++;if(k == 3) b ++;
          if(g > r && s[3] == 'G') dp[3][i][j][k] = g;
          if(g == r && s[3] == 'B') dp[3][i][j][k] = g;
          if(g < r && s[3] == 'R') dp[3][i][j][k] = g;
      }
      
    /*  
    for(int i = 1;i <= 3;i ++)
     for(int j = 1;j <= 3;j ++)
      for(int k = 1;k <= 3;k ++)  
       printf("%d\n",dp[3][i][j][k]);
    */ 
    
    for(int now = 4;now <= n;now ++)
     for(int i = 1;i <= 3;i ++)//假设出i,j,k三个岛颜色的所有情况
      for(int j = 1;j <= 3;j ++)
       for(int k = 1;k <= 3;k ++)
       {
           int r = 0,g = 0,b = 0;//统计红绿黑颜色情况
           if(i == 1) r ++;if(i == 2) g ++;if(i == 3) b ++;
           if(j == 1) r ++;if(j == 2) g ++;if(j == 3) b ++;
           if(k == 1) r ++;if(k == 2) g ++;if(k == 3) b ++;
           
           for(int l = 1;l <= 3;l ++)
           {
               if(dp[now - 1][j][k][l] == - 1) continue;//不存在合法方案直接跳过
               if(g > r && s[now] == 'G') dp[now][i][j][k] = max(dp[now][i][j][k],dp[now - 1][j][k][l] + (i == 2));
               if(g == r && s[now] == 'B') dp[now][i][j][k] = max(dp[now][i][j][k],dp[now - 1][j][k][l] + (i == 2));
               if(g < r && s[now] == 'R') dp[now][i][j][k] = max(dp[now][i][j][k],dp[now - 1][j][k][l] + (i == 2)); 
           }
       }

    int res = - 1;
    for(int i = 1;i <= 3;i ++)
     for(int j = 1;j <= 3;j ++)
      for(int k = 1;k <= 3;k ++)
       res = max(res,dp[n][i][j][k]);
    printf("%d\n",res);
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cold啦啦啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值