##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;
}
```