🔔目录
蓝桥杯冲冲冲~
🔖蓝桥杯每日练习(猴子分香蕉,等差数列,平方序列,倍数问题)
🔖蓝桥杯每日练习(纯质数(筛法的应用),最少砝码,灌溉)
🔖蓝桥杯每日练习(年龄巧合,纸牌三角形,取球游戏)
🚶引言
今天继续蓝桥杯每日练习~ 今天的题目可以学到四个知识点:状态压缩,求约数个数,同余,前缀和。今天的题目包含的知识点还是挺多的,用到的时候也是很多的。状态压缩是包括状态压缩dp在内的很多算法的基础,属于是必会知识点。前缀和是一个很好用的工具,在很多题目中都有应用。因子数和同余更是单独的出一道题目的可能性都很大。所以今天的知识点都比较重要,期待你能通过我们题解有一个好的掌握~我是学习算法的小菜鸡,每天一个小知识点,我们国赛见~
🚀今日题目
💓最大乘积
状态压缩 |
题目描述
把 1 ~ 9 这 9 个数字分成两组,中间插入乘号, 有的时候,它们的乘积也只包含 1 ~ 9 这 9 个数字,而且每个数字只出现 1 次。
原题传送门
解题报告
直接对这九个数字进行全排列,然后枚举插入乘号的位置,然后检查答案就可以了。
然后对答案检查的方法,我们可以状态压缩一下,用一个32位整型的每一位表示一个状态,结合位运算的有关知识实现对每一位的标记。这样就可以实现对每一位的标记以及只能标记一次,有重复直接返回false。并且要求了九个数必须都出现,那么我们可以想一下九位都标记以后是什么样子?就是二进制下的 1111111110 1111111110 1111111110(因为我们没有用到0,所以最后一位是0,其他位上有标记,所以是1),所以在没有冲突下返回是否等于前面的数字,也就是十进制下的1022。
参考代码(C++版本)
#include <iostream>
#include <string>
using namespace std;
int ans,st[15],res;
string ch;
bool check(int k) {
long long mp = 0;
while (k) {
int cnt = k % 10;
if ((mp >> cnt)& 1) return false;
mp |= 1 << cnt;
k /= 10;
}
return mp==1022;
}
int f(int n)
{
int w = 0;
for (int i = 0; i <= n; i++)
{
w = w * 10 + ch[i] - '0';
}
return w;
}
int b(int n)
{
int w = 0;
for (int i = n + 1; i < 9; i++)
{
w = w * 10 + ch[i] - '0';
}
return w;
}
void dfs(int k) {
if (k == 9) {
ch = to_string(res);
for (int n = 0; n <= 7; n++)
{
int a = f(n), c = b(n);
if (check(a * c)) ans=max(ans,a*c);
}
return;
}
for (int i = 1; i < 10; i++) {
if (k == 0 && i == 0) continue;
if (!st[i]) {
st[i] = 1;
res = res * 10 + i;
dfs(k + 1);
res /= 10;
st[i] = 0;
}
}
}
int main()
{
dfs(0);
cout << ans << endl;
return 0;
}
🌟阶乘约数
🌱题目描述
定义阶乘 n! = 1 × 2 × 3 × · · · × n
请问 100! (100 的阶乘)有多少个正约数。
原题传送门
求约束个数 |
🌴解题报告
求约数个数是一个很经典的问题。
暴力
这是最直接就能想到的了,n的因子数就是枚举一次1~n的所有整数,如果能被n整除,因子数加一.这样时间复杂度是O(n)的
优化
注意到如果i是n的因子,那么n/i也是n的一个因子。因此,我们不需要枚举1~n的整数而只需要枚举1~ n \sqrt{n} n就可以了。这样时间复杂度是O( n \sqrt{n} n)的。
公式计算
将一个整数n分解质因数以后能得到 n = n 1 p 1 + n 2 p 2 + n 3 p 3 + . . . n=n1^{p1}+n2^{p2}+n3^{p3}+... n=n1p1+n2p2+n3p3+...然后我们来看他的因子长什么样。他的因子分解质因子得到的质因子一定包含在n中(他是由n分出来的嘛)所以我们就有因子数就是n中每一个质因子取x个乘起来得到的。nk一共可以取0-pk个。所以就有 s u m = ( 1 + p 1 ) + ( 1 + p 2 ) + ( 1 + p 3 ) + . . . sum=(1+p1)+(1+p2)+(1+p3)+... sum=(1+p1)+(1+p2)+(1+p3)+...直接计算,时间复杂度就变成O(1)了。
了解了约数个数的求法以后我们来考虑一下这道题应该选取那种方法。一看数据是有
100感叹号
这么大,毫无疑问我们要选择公式法。又注意到一个数字分解质因数的结果和他的因子分解质因数的乘积相等,所以我们可以直接对1~100的所有数字分解一下质因子,然后把出现次数加起来( a x ∗ a y = a ( x + y ) a^x*a^y=a^{(x+y)} ax∗ay=a(x+y)),然后直接利用上面的公式计算即可。
🌵参考代码(C++版本)
#include <iostream>
#include <map>
#define int long long
using namespace std;
map<int, int>mp;
void check(int k) {
for (int i = 2; i <= k; i++) {
while (k % i == 0) {
mp[i]++;
k /= i;
}
}
}
void find() {
for (int i = 2; i <= 100; i++) {
check(i);
}
}
signed main()
{
find();
int ans = 1;
for (auto t : mp) {
ans *= (t.second + 1);
}
cout << ans << endl;
return 0;
}
含 2 天数
题目描述
小蓝特别喜欢 2,今年是公元 2020 年,他特别高兴,因为每天日历上都可以看到 2。
如果日历中只显示年月日,请问从公元 1900 年 1 月 1 日到公元 9999 年 12 月 31 日,一共有多少天日历上包含 2。即有多少天中年月日的数位中包含数字 2。
原题传送门
枚举 |
🚗解题报告
枚举年份然后如果发现年份里面有2,那么我们就什么也不用看了这一年中的每一天一定都有2,所以我们就看看这一年是不是闰年,闰年答案加366,平年答案加365。如果年份里面没有2这个数字,那么我们就对月份进行枚举。如果月份里面有2,那么整月也是答案,预处理好每一个月的天数直接加就可以;如果月份也没有2,那么这个月只会有12天有2(2月特殊,但是因为2月月份里有2已经整月算过了,所以不用考虑)。
参考代码(C++版本)
#include <iostream>
using namespace std;
int ans;
int day[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
bool check(int k) {
while (k) {
if (k % 10 == 2) return true;
k /= 10;
}
return false;
}
int main()
{
for (int i = 1900; i <= 9999; i++) {
if (check(i)) {
if (i % 400 == 0 || (i % 100 != 0 && i % 4 == 0)) ans += 366;
else ans += 365;
continue;
}
for (int j = 1; j <= 12; j++) {
if (check(j)) {
ans += day[j];
if (j == 2 && (i % 400 == 0 || (i % 100 != 0 && i % 4 == 0))) ans++;
}
else ans += 12;
}
}
cout << ans << endl;
return 0;
}
🙵k倍区间
题目描述
给定一个长度为N的数列,A1, A2, … AN,如果其中一段连续的子序列Ai, Ai+1, … Aj(i <= j)之和是K的倍数,我们就称这个区间[i, j]是K倍区间。
你能求出数列中总共有多少个K倍区间吗?
原题传送门
同余与前缀和 |
🚇解题报告
我们求一个区间 ( a [ l ] + a [ l + 1 ] + a [ l + 2 ] + . . . + a [ r ] ) % k = = 0 (a[l]+a[l+1]+a[l+2]+...+a[r])\%k==0 (a[l]+a[l+1]+a[l+2]+...+a[r])%k==0其实可以对所有数字里面就提前抛去k的整倍数,也就是对k的余数。所以我们就可以直接求 ( a [ l ] % k + a [ l + 1 ] % k + a [ l + 2 ] % k + . . . + a [ r ] % k ) % k = = 0 (a[l]\%k+a[l+1]\%k+a[l+2]\%k+...+a[r]\%k)\%k==0 (a[l]%k+a[l+1]%k+a[l+2]%k+...+a[r]%k)%k==0的区间。因为对k取余以后的结果一定落在 0 0 0 ~ k − 1 k-1 k−1的范围内,这样数据就会变小,我们就对问题做了
第一次优化
。
然后既然用到了区间和,那么我们可以很容易的想到前缀和这个工具。前缀和怎么用呢?我们做一个 s u m sum sum数组,用 s u m [ i ] sum[i] sum[i]来表示 a [ 1 ] + a [ 2 ] + a [ 3 ] + . . . + a [ i ] a[1]+a[2]+a[3]+...+a[i] a[1]+a[2]+a[3]+...+a[i]。要达到这个目的我们可以让 s u m [ i ] = s u m [ i − 1 ] + a [ i ] sum[i]=sum[i-1]+a[i] sum[i]=sum[i−1]+a[i]这样递推的来处理出前缀和数组。对于 l l l~ r r r这个区间的区间和我们就可以直接用 s u m [ r ] sum[r] sum[r]减去 s u m [ l − 1 ] sum[l-1] sum[l−1](具体是为什么可以结合定义看一下)。所以预处理前缀和,就是我们计算区间和不再用一个循环一个一个加,这就是
第二次优化
。
有了这些准备工作以后我们怎么做这道题呢?我们用一个数组标记一下前面出现过的余数(这些余数都是从一开始的区间余数),然后如果当前枚举到的余数前面出现过,就说明这个区间和对k取余结果为0。
参考代码(C++版本)
#include <iostream>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int k[N], q[N], sum[N], cnt[N];
int n, m, ans;
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> q[i];
for (int i = 1; i <= n; i++) k[i] = q[i] % m;
for (int i = 1; i <= n; i++) sum[i] = (k[i] + sum[i - 1]) % m;
for (int i = 1; i <= n; i++) ans += cnt[sum[i]], cnt[sum[i]]++;
cout << ans + cnt[0]<< endl;
}