牛客周赛Round39 B~E


##B
贪心的做法 确实没学过 被卡住了
但实际上思路很好理解
![B-小红不想做鸽巢原理_牛客周赛-Round-39.png](https://cdn.acwing.com/media/article/image/2024/04/08/407567_6b86cd8ff5-B-小红不想做鸽巢原理_牛客周赛-Round-39.png) 
题目要求从盒子里每次取出k个球 直到盒子中剩余的球少于k个时 剩下小球颜色种类最小值
实际上我们可以转换成从盒子里取出 **$m = $ ${Σa[i]}$ ${mod}$ $k$**  个小球 这个式子相当于表示**盒子里取出k个球后剩下的球**
我们是要保证这几个球的**颜色种类数**最小
那我们怎么让这几个球的颜色种类最小呢?
就是从盒子里某一颜色**个数最多**的小球开始取这几个球
如果这一个颜色就能取完 那我们小球颜色种类就只有1种
如果取不完
就向第二多的颜色小球去取 直到把我们要求的取出的$m$个小球取完即可
```
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int a[N];
int n, k;
long long sum;
long long cnt;

int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i ++ ) 
    {
        scanf("%d", &a[i]);
        sum += a[i];
    }
    sort(a + 1, a + n + 1); //将每一颜色的小球数量从小到大排序 注意输入的时候i从1开始的 那么这里排序首地址也是a+1
    sum %= k; //按转换后的题意 找出我们需要取出几个球
    if (sum == 0) //如果一个小球都不需要取出 直接返回答案 盒子内没有剩余小球
        {
            puts("0");
            return 0;
        }
    for (int i = n; i > 0; i -- ) //按要求将某一颜色小球个数 按从大到小查询
    {
        if (sum > a[i]) //如果需要取出的小球个数比当前颜色小球个数多
        {
            cnt ++ ; //方案数加1
            sum -= a[i]; //取出该颜色所有球
        }
        else //如果当前a[i]比sum大 证明当前小球都可以从这一个颜色里取
        {
            cnt ++ ;
            printf("%lld", cnt);
            return 0;
        }
    }
    
}
```
##C
![C-小红不想做完全背包(easy)_牛客周赛-Round-39.png](https://cdn.acwing.com/media/article/image/2024/04/08/407567_44367162f5-C-小红不想做完全背包(easy)_牛客周赛-Round-39.png) 
想不到这题这么巧妙
他既然规定了我们只需要在n种物品中取出3的倍数即可
那我们将每个物品都**模上3** 那问题就转化为从这几个数中求出**3的倍数**即可
对于模上3后相同价值的物品 我们将其看成同一个 由于所有数模上3之后的结果只有0 1 2 
每个组合求一下{1,1,1},{1,2},{0} 这三种3的倍数的情况
```
#include <bits/stdc++.h>

using namespace std;

const int N = 2010;

int a[N];
int n, p;

int main()
{
    scanf("%d%d", &n, &p);
    map<int, int> mp; //我们用map来记录每个数模上3之后剩余的数 第二位记录其个数 
                      //map好像默认就是两个元素 所以不能用typedef pair<int, int> PII;
    for (int i = 1; i <= n; i ++ ) 
    {
        scanf("%d", &a[i]);
        a[i] %= 3;
        mp[a[i]] ++ ; //将取模后的a[i]存入map 这个数的个数++
    }
    
    if (mp[0]) cout << 1 << endl; //如果取模后的数是0 表示其本身就是3的倍数 取一个物品就能实现题目要求
    else if (mp[1] && mp[2]) cout << 2 << endl; //如果同时存在取模后为2和取模后为3的数 其相加后就是三的倍数 需要取两个
    else if (mp[1] >= 3) cout << 3 << endl; //三个取模后为1的数也能构成3的倍数
    else if (mp[2] >= 3) cout << 3 << endl; //三个取模后为2的数也能构成3的倍数
    
    return 0;
}
```
##D
本题用dp背包 和 最短路两种方法做
![D-小红不想做完全背包-(hard)_牛客周赛-Round-39.png](https://cdn.acwing.com/media/article/image/2024/04/08/407567_3dff16b8f5-D-小红不想做完全背包-(hard)_牛客周赛-Round-39.png) 
####dp背包
观察题目我们要 求的是 选出的物品之和 mod p = 0的最小选择物品数
**dp[i] -- 在所有物品中选择的和满足%p=i的最小选择数个数**
那么我们最终求得的答案就是dp[0] dp[0]就是mod p = 0的最小选择个数
对于每个物品 我们先将其模上p 对答案需要选择的物品个数没有影响 还能缩小范围
a[i]%p之后,一定就会小于p。假设a[i]%p = m。
那么dp[m]就可以初始化为1 因为m%p就是m自身 我们至少已经有了一个物品a[i]符合了
那么我们就可以初始化dp数组 --- dp[a[i]%p] = 1
状态转移方程
对于每一个a[i]来说 他可以走到的范围就是0~p 利用j遍历 0~p 我们就可以找到dp[a[i] + j]
dp[a[i] + j]就是模p等于a[i] + j的选择个数
在dp[j]的基础上+物品a[i] 就可以产生mod p = a[i] + j 也就是d[j] + 1
~~说的太抽象了~~
对于模完p的每一个a[i],它可能通过一步走到的数的范围在(a[i] + (0 ~ p))%p。
那么让j 遍历 0~p 。nex(可能到达的点)= (a[i]+(0~p))%p 
dp[ne] = min(dp[ne],dp[j]+1).  -- 在满足j的条件下 我只需要在数组中多选一个a[i]就行了。
https://blog.csdn.net/huhaitao0313_/article/details/137481708
```
#include <bits/stdc++.h>

using namespace std;

const int N = 2010;

int a[N], f[N];  //f[i]表示在所有的数中选若干个使得%p=i的最小选择数

int n, p;
int nex;

int main()
{
    memset(f, 0x3f, sizeof f); //注意 我们要给f数组初始化为无穷 默认所有都选不到
    
    scanf("%d%d", &n, &p);
    
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    
    for (int i = 1; i <= n; i ++ )
    {
        a[i] %= p;
        f[a[i]] = 1; //初始化 模上p之后a[i]的值 在当前数组里面 我最少选择1个数就可以满足它%p=a[i] 也就是选它本身
        for (int j = 0; j < p; j ++ ) //最小取p-1 如果偏移j就又回到f[i]了
        {
            nex = (a[i] + j) % p; //在a[i]的基础上偏移j 也就是找a[i]所有可能到达的位置
            f[nex] = min(f[nex], f[j] + 1); //偏移后的点的选择物品数量最小值 在它自身 和 偏移量j需要选择的物品数量 + 再多选择物品a[i]后 的最小值取得
        }
    }
    
    printf("%d\n", f[0]); //答案就是mod p等于0 需要选择的最少物品数量
}
```
**与传统背包题不同的是 这里只是将i当作一个中间工具 在他周围寻找我们能dp到的点**


----------

####最短路做法 利用广度优先搜索
寻找 (x + a[i]) % p == 0的最短路径 x表示取模到0~p中的一个数最少选择的物品数量d[x] 我们利用x + a[i] % p去表示0~p中d[x + a[i] % p] 他最少需要的物品数量就是x的物品数量加上a[i]这一个物品
当我们搜到d[0]就可以退出了 他就是%p等于0最少需要的物品个数 也就是答案
最先出现的d[0]肯定是最短的 ~~这个我不太懂~~

```
#include <bits/stdc++.h>

using namespace std;

const int N = 2010;

typedef pair<int, int> PII;

int a[N], d[N];

int n, p, ans, cnt;
queue<PII> q;

int main()
{
    scanf("%d%d", &n, &p);
    
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        x = x % p; //把x和a[i]都给模p了 减小数据范围
        if (d[x] == 0)
        {
            a[ ++ cnt] = x; //只存不重复的a[i]
            d[x] = 1; //初始化 取模到x至少选择他自己
            q.push({x, 1}); //将x和取模p=x的最少物品数量放进队列
        }
    }
    n = cnt; //题目里的n已经改变了 我们只存了不重复的a[i]
    while (q.size())
        {
            auto t = q.front();
            int x = t.first, y = t.second;
            q.pop();
            //cout << x << " " << y << endl;
            //对一个x枚举每一个a[i] 表示d[nx]
            for (int i = 1; i <= n; i ++ )
            {
                int nx = (x + a[i]) % p;
                if (d[nx] == 0) //如果位于0~p的这个nx还没有被算过 我们就利用d[x]去算他
                {
                    d[nx] = y + 1;
                    q.push({nx, y + 1});
                    
                }
            }
            if(d[0]) //如果已经算出d[0] 直接break;
            {
                ans = d[0];
                break;
            }
                 
        }
    
    printf("%d\n", ans); 
}
```
我目前是这样想的 为什么越先找到的d[0]就越短呢 因为你每在while(q.size())循环里**多待一次** 你的路径就会变成y+1 初始的y是1 所以肯定是**最小**的 后面根据不断找到的nx d[nx]就会**递增**
当你利用d[nx]找到的d[0] 肯定也就会变大
所以肯定是越先找到的路径越短

##E 枚举 剪枝(加上特殊条件限制 减少枚举次数)
![E-小红不想做莫比乌斯反演杜教筛求因子和的前缀和_牛客周赛-Round-39.png](https://cdn.acwing.com/media/article/image/2024/04/08/407567_91d05a10f5-E-小红不想做莫比乌斯反演杜教筛求因子和的前缀和_牛客周赛-Round-39.png) 
如果我们分别暴力枚举算出的长度不小于n 宽度不小于m 高度不小于p 肯定会超时
我们加上一些特殊条件优化 如果不符合 直接跳出本次循环就可以减少枚举次数
由于面积是题目给的,利用面积公式我们已知两个量的时候就可以求出另外一个量
**可以转化为**,在枚举每一个长和宽的时候,他对应的**合法高度**为多少,将合法高度累加即是答案所求的合法蛋糕数量
![8a0ee5420c863e94464fb3d6cd49e25.jpg](https://cdn.acwing.com/media/article/image/2024/04/08/407567_a8729d26f5-8a0ee5420c863e94464fb3d6cd49e25.jpg) 

**核心代码**
`if (sum > 0 && sum % (2 * (i + j)) == 0)`  `k` = `sum % (2 * (i + j)) == 0 `这个式子表示的是高度k是合法的 也就是当前剩余奶油量 - k*(2 * (i + j))等于0 能使用完这些奶油 就是看我们当前枚举的`i`和`j`能不能找到一个整数高度去使用完这些剩余的奶油
```
#include <bits/stdc++.h>

using namespace std;

int n, m, p;
int x; //最终要使用完这些奶油
int ans;

int main()
{
    scanf("%d%d%d%d", &n, &m, &p, &x);
    
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ )
        {
            int sum = x - i * j; //剩余奶油量
            //下面是核心代码
            if (sum > 0 && sum % (2 * (i + j)) == 0) //如果剩余奶油量大于0 并且 sum % (2 * (i + j)) == 0          {
            {
                int k = sum / (2 * (i + j)); //算出高度
                if (k <= p) ans ++ ; //如果该高度小于题目的高度限制 那我们就可以构成一个蛋糕 蛋糕数++
            }
        }
    }
    
    printf("%d\n", ans);
    
    return 0;
}
```

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值