这里写目录标题
- [【数学1】基础数学问题 - 题单 - 洛谷](https://www.luogu.com.cn/training/117)
- [P1143 进制转换](https://www.luogu.com.cn/problem/P1143)
- [P1469 找筷子](https://www.luogu.com.cn/problem/P1469)
- [P1100 高低位交换](https://www.luogu.com.cn/problem/P1100)
- [P1017 [NOIP2000 提高组] 进制转换](https://www.luogu.com.cn/problem/P1017)
- [P1866 编号](https://www.luogu.com.cn/problem/P1866)
- [P2822 [NOIP2016 提高组] 组合数问题](https://www.luogu.com.cn/problem/P2822)
- [P2789 直线交点数](https://www.luogu.com.cn/problem/P2789)
- [P3913 车的攻击](https://www.luogu.com.cn/problem/P3913)
- [P1317 低洼地](https://www.luogu.com.cn/problem/P1317)
- [P2638 安全系统](https://www.luogu.com.cn/problem/P2638)
- [P1246 编码](https://www.luogu.com.cn/problem/P1246)
- [P2926 [USACO08DEC]Patting Heads S](https://www.luogu.com.cn/problem/P2926)
- [P3383 【模板】线性筛素数](https://www.luogu.com.cn/problem/P3383)
- [P1835 素数密度](https://www.luogu.com.cn/problem/P1835)
- [P1029 [NOIP2001 普及组] 最大公约数和最小公倍数问题](https://www.luogu.com.cn/problem/P1029)
- [P1072 [NOIP2009 提高组] Hankson 的趣味题](https://www.luogu.com.cn/problem/P1072)
- [P1069 [NOIP2009 普及组] 细胞分裂](https://www.luogu.com.cn/problem/P1069)
- [P1572 计算分数](https://www.luogu.com.cn/problem/P1572)
- [P4057 [Code+#1]晨跑](https://www.luogu.com.cn/problem/P4057)
- [P1414 又是毕业季II](https://www.luogu.com.cn/problem/P1414)
- [P3601 签到题](https://www.luogu.com.cn/problem/P3601)
- [P2651 添加括号III](https://www.luogu.com.cn/problem/P2651)
- [P2660 zzc 种田](https://www.luogu.com.cn/problem/P2660)
- [P1403 [AHOI2005]约数研究](https://www.luogu.com.cn/problem/P1403)
- [P1593 因子和](https://www.luogu.com.cn/problem/P1593)
【数学1】基础数学问题 - 题单 - 洛谷
P1143 进制转换
做这道题真的是几经坎坷,调了老半天,可能是我基础不牢吧~~(蒟蒻)~~?
基本思路就是把 n n n 进制数转化为 10 10 10 进制数,然后再转化为 m m m 进制数。
先将 n n n 进制数用字符串 s s s 读入,然后倒序枚举每个数,将其转化为 10 10 10 进制数;开一个数组 a n s ans ans 用来记录最终的结果,用while循环得到结果;最后还是倒序枚举,分情况输入结果。
另外,最终的结果其实也可以用栈来存储。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
//#include <string>
using namespace std;
const int N = 30;
int main()
{
int n, m, num = 0;
string s;
//输入三个参数
cin >> n;
cin >> s;
cin >> m;
//这一步可以得到10进制数num
int base = 1; //基数,每个位上的权值
for(int i = s.length() - 1; i >= 0; i --) //倒序
{
char c = s[i];
if('0' <= c && c <= '9')
num += (c - '0') * base;
else
num += (c - 'A' + 10) * base;
base *= n; //基数记得更新
}
//这一步可以得到最终答案,存在数组ans里
int count = 0; //中间变量,最后弄完,count的值为答案的位数
int ans[N];
while(num) //不为0就继续
{
ans[count ++] = num % m;
num /= m;
}
for(int i = count - 1; i >= 0; i --) //还是倒序
if(ans[i] > 9) //注意这里要分情况输入
cout << (char)(ans[i] - 10 + 'A');
else
cout << ans[i];
return 0;
}
感谢大佬kIG7Z8oP的精致题解,弄得我抑郁了半天。这里可以用快读和快写的思想。
思路也是差不多:用 r e a d read read 函数读入数据把 n n n 进制数转化为 10 10 10 进制数,用 w r i t e write write 函数再转化为 m m m 进制数并输出。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int read(int x) //读入n进制数,转化为10进制数并赋给num
{
int ans = 0, sign = 1;
char c = getchar(); //事先一定要先读入
if(c == '-') //这道题这里没用
sign = -1;
while(!('0' <= c && c <= '9') && !('A' <= c && c <= 'F')) //这一步显得至关重要,可以排除空格和换行符的影响
c = getchar();
while('0' <= c && c <= '9' || 'A' <= c && c <= 'F')
{
if('0' <= c && c <= '9') //分情况更新ans的值
ans = ans * x + c - '0';
else
ans = ans * x + 10 + c - 'A';
c = getchar();
}
return ans * sign;
}
void write(int num, int x) //将num转化为m进制数并输出
{
if(!num) //递归边界条件,如果为0,到达边界,返回
return;
write(num / x, x); //由于是倒序输出,所以先递归,后输出
int t = num % x;
if(t < 10) //分情况输出字符
putchar(t + '0');
else putchar(t - 10 + 'A');
}
int main()
{
int n, m, num;
cin >> n;
num = read(n); //读入n进制数,转化为10进制数并赋给num
cin >> m;
write(num, m); //将num转化为m进制数并输出
return 0;
}
P1469 找筷子
万万没想到,这道题居然如此简单。此题算是刷新了我对位运算特别是异或运算的认识。
异或有两个性质:
- k k k 个相同的数的异或和,当 k k k 为奇数时,结果是这个数本身;当 k k k 为偶数时,结果是 0。
- 任何数与 0 0 0 的异或值是它本身。
个人理解哈,异或中的0相当于乘法中的1,即:0 ^ 0 = 0, 0 ^ k = k
;1 * 1 = 1, 1 * k = k
。但是两者也有很大的不同:k ^ k = 0
;而k * k != 1
。
且异或运算满足归零律、恒等律、交换律、结合律、和自反 a ^ b ^ a = b
。
根据题意,除了一根落单的筷子外,其它的筷子都是成双成对的(即长度是相同的)。根据异或的性质,成对的筷子的异或值为0,且许多对成对的筷子的异或值也为0(因为0 ^ 0 = 0
),所以当所有筷子的长度一起异或得到的值就是多余的那根筷子的长度。
位运算一定要注意它的优先级,极易出错。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int main()
{
int n;
cin >> n;
int ans = 0; //ans记得初始化为0
for(int i = 1; i <= n; i ++)
{
int a;
cin >> a;
ans ^= a;
}
cout << ans << endl;
return 0;
}
P1100 高低位交换
读懂题意后可知,将低16位往左移至高16位需要用到左移运算符,将高16位往右移至低16位需要用到右移运算符。还有在做左移或者右移的时候,多余的位数就已经自动溢出了,所以可以省略不写。
对了,这道题的数据类型是 unsigned int
。还有不要忘了左移右移较低的优先级。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int main()
{
unsigned int x;
cin >> x;
unsigned int a = x << 16, b = x >> 16; //这里似乎不能写成 x << 16 + x >> 16, 左移右移低得可怜的优先级
cout << a + b << endl;
return 0;
}
P1017 [NOIP2000 提高组] 进制转换
此题算是增加了对数学的认识。将整数用基数为负数的进制表示。
首先,不管对于什么语言,
被除数=商*除数+余数
例如在C++里, − 15 m o d − 2 = − 1 -15 \mod -2=-1 −15mod−2=−1, − 15 ÷ − 2 = 7 -15 \div-2=7 −15÷−2=7 ,而 7 × − 2 + ( − 1 ) = − 15 7 \times -2+ (-1) =-15 7×−2+(−1)=−15 。
但是因为我们是不断取余数倒序为转换结果,所以余数不能出现负数。此时,我们只需要将商+1,余数 - 除数即可,因为余数(绝对值)一定小于除数,所以这样就可以将余数装换为正数。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20; //这里的数据范围不会超过20,即便是以-2为基数
int main()
{
int n, r;
cin >> n >> r;
cout << n << "="; //因为后面n的值发生了变化,所以这边先输出为妙
int ans[N], pos = 0; //用ans数组存储答案,pos中间变量,帮助记录结果
while(1)
{
if(n % r >= 0) //
{
ans[pos ++] = n % r;
n /= r;
}
else
{
ans[pos ++] = n % r - r;
n = n / r + 1;
}
if(!n)
break;
}
for(int i = pos - 1; i >= 0; i --)
{
if(ans[i] > 9)
cout << (char)(ans[i] - 10 + 'A');
else
cout << ans[i];
}
printf("(base%d)", r);
return 0;
}
另外,这道题也可以用递归函数来做。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
void convert(int x, int base)
{
if(!x)
return;
int remainder = x % base;
if(remainder < 0) //余数出现负数的情况,提前处理
remainder -= base, x += base;
convert(x / base, base); //递归
//因为结果为余数倒序输出,输出要写在递归后面,不然会顺序输出
if(remainder > 9) //输出得分情况
cout << (char)(remainder - 10 + 'A');
else
cout << remainder;
}
int main()
{
int n, r;
cin >> n >> r;
cout << n << "="; //因为后面n的值发生了变化,所以这边先输出为妙
convert(n, r);
printf("(base%d)", r);
return 0;
}
P1866 编号
基本思路:先从小到大排序,然后数字小的先选,选完后下一个再选。举个例子:
设 n = 5 n=5 n=5,5个数分别为 13 , 9 , 1 , 65 , 42 13, 9, 1, 65, 42 13,9,1,65,42。
排完序后为: 1 , 9 , 13 , 42 , 65 1, 9, 13, 42, 65 1,9,13,42,65。
第一个兔子有1种选择,即为1。
第二个兔子有 9 − 1 = 8 9-1=8 9−1=8(种)选择(去掉1种)
第三个兔子有 13 − 1 = 12 13-1=12 13−1=12(种)选择(去掉2种)
第四个兔子有 42 − 1 = 41 42-1=41 42−1=41(种)选择(去掉3种)
第五个兔子有 65 − 1 = 64 65-1=64 65−1=64(种)选择(去掉4种)
所以最终结果为: 1 × 8 × 12 × 41 × 64 m o d 1000000007 1 \times 8 \times 12 \times 41 \times 64 \mod 1000000007 1×8×12×41×64mod1000000007 。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL; //要开long long,因为mod的值比较大
const int N = 60, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
int maxn[N];
for(int i = 0; i < n; i ++)
cin >> maxn[i];
sort(maxn, maxn + n); //从小到大排序
LL ans = 1;
for(int i = 0; i < n; i ++)
ans = (ans * (maxn[i] - i)) % mod; //核心公式
cout << ans << endl;
return 0;
}
P2822 [NOIP2016 提高组] 组合数问题
需掌握组合数的基本两种求解方法(通项公式,递推公式),根据数据范围选定方法。
组合数公式: C n m = C n − 1 m + C n − 1 m − 1 C _{n} ^{m} = C _{n - 1} ^{m} + C _{n - 1} ^{m - 1} Cnm=Cn−1m+Cn−1m−1,一般公式: C n m = n ! m ! ( n − m ) ! C _{n} ^{m} = \frac {n!} {m!(n - m)!} Cnm=m!(n−m)!n!。
初始做法:递归做法,无法前缀和优化,只能得到65分,其它点全部TLE。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 2e3 + 10;
LL com_num[N][N];
int t, k;
LL C(int n, int m) //递归+记忆化,表示从n件物品中选m个物品
{
if(com_num[n][m])
return com_num[n][m];
if(n < m)
return 0;
if(m == 0 || n == m)
return com_num[n][m] = 1;
return com_num[n][m] = (C(n - 1, m) + C(n - 1, m - 1)) % k;
}
int main()
{
cin >> t >> k;
while(t --)
{
int n, m;
cin >> n >> m;
int ans = 0;
for(int i = 0; i <= n; i ++)
for(int j = 0; j <= min(i, m); j ++)
if(C(i, j) % k == 0)
ans ++;
cout << ans << endl;
}
return 0;
}
正解:前缀和+递推
这道题的前缀和和之前的前缀和不同,这是求三角形的前缀和,之前的是求矩形的前缀和。
思路是先递推求出组合数,后求出前缀和;或者求组合数的同时求前缀和。
前缀和,有效减少查询统计时的复杂度,每一次查询 O ( n ) O(n) O(n) 降到 O ( 1 ) O(1) O(1)。
三角形前缀和的特点:当
j
=
i
j = i
j=i 时,会出现 sum[i - 1][j]
的情况,如果不特殊处理,则其值早就初始化为0了,所以得提前处理在上一行最右边一个数计算完后,就赋值给再右边的那个数,这样下一行计算时不会为空。
求前缀和与递推分离的代码:
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
//typedef long long LL;
const int N = 2e3 + 10;
int com_num[N][N], sum[N][N]; //组合数数组和前缀和数组,作为全局变量,初始化为0
//com_num[i][j]表示从i件物品中选j个物品的组合数,sum[i][j]表示前缀和以i为终点和以j为终点的答案数目
int main()
{
int t, k;
cin >> t >> k;
//求组合数
for(int i = 0; i <= 2e3; i ++)
{
com_num[i][0] = 1; //边界值初始化
for(int j = 1; j <= i; j ++)
com_num[i][j] = (com_num[i - 1][j - 1] + com_num[i - 1][j]) % k;
}
//求前缀和
for(int i = 1; i <= 2e3; i ++) //这里绝对不能写N,因为会数组越界
{
for(int j = 1; j <= i; j ++) //当j = i时,会出现sum[i - 1][j]的情况,所以得提前处理
{
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
if(com_num[i][j] % k == 0)
sum[i][j] ++;
}
sum[i][i + 1] = sum[i][i]; //这一步相当重要,一定要想清楚了,下次累加时不为空,继承
}
while(t --)
{
int n, m;
cin >> n >> m;
cout << sum[n][min(n, m)] << endl; //这一步不要出错
}
return 0;
}
求前缀和与递推相结合的代码:
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
//typedef long long LL;
const int N = 2e3 + 10;
int com_num[N][N], sum[N][N]; //组合数数组和前缀和数组,作为全局变量,初始化为0
int main()
{
int t, k;
cin >> t >> k;
for(int i = 0; i <= 2e3; i ++)
{
com_num[i][0] = 1; //边界值初始化
for(int j = 1; j <= i; j ++)
{
com_num[i][j] = (com_num[i - 1][j - 1] + com_num[i - 1][j]) % k; //求组合数
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1]; //求前缀和
if(com_num[i][j] % k == 0)
sum[i][j] ++; //满足题意,加上自己
}
sum[i][i + 1] = sum[i][i]; //此式漏掉前缀和求出来不对,继承
}
while(t --)
{
int n, m;
cin >> n >> m;
cout << sum[n][min(n, m)] << endl; //这一步不要出错,有可能m > n,此时就是sum[n][n]
}
return 0;
}
P2789 直线交点数
数学+递推
见大佬Y_B_Y的博客。
n n n 条互不平行直线且无三线共点,交点数为 n × ( n − 1 ) 2 \frac {n \times (n-1)} {2} 2n×(n−1)条。又根据无三线共点这一条件,可以得出1条直线与n条直线不平行,那么它一定会与这 n n n 条直线交于 n n n 个点。当所有直线都平行时,交点数为0,无论 n n n 取何值都成立。假设共有 n n n 条直线,当有 i i i 条直线相平行时,这 i i i 条直线中的任意一条与剩余直线都有交点,因此此时这 i i i 条直线的所有交点数为 i ∗ ( n − i ) i * (n - i) i∗(n−i) ,而此 n n n 条直线所有的交点数还得考虑剩余的 n − i n - i n−i 条直线之间的交点数。
我们设 f[i][l]
为有
i
i
i 条直线时,交点数为
l
l
l 的情况存不存在,如果存在为1,所以初始化为 f[i][0] = 1
(一定存在无交点的情况(互相平行)),其他为0,我们可以枚举直线数
i
i
i ,未放飞自我的直线数
j
j
j ,放飞自我的直线**产生的交点数
k
k
k **,那么我们可以得出:
f[i][j * (i − j) + k] = f[i][j * (i − j) + k] || f[i − j][k]
(||
运算表示两边只要有一个为1,结果就为1)。
我们需要枚举 j j j 和 k k k 。 k k k 为交点个数。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 30, M = 350; //最大为25 * (25 - 1) / 2,故取350
int main()
{
int n;
cin >> n;
int dp[N][M];
memset(dp, 0, sizeof dp); //初始化
for(int i = 1; i <= n; i ++)
{
dp[i][0] = 1; //边界条件,所有直线平行的情况是存在的
for(int j = 1; j <= i; j ++) //枚举平行的线段的数量,最少为1,即没有平行线段组存在
for(int k = 0; k <= (i - j) * (i - j - 1) / 2; k ++) //k有枚举的上界(所有直线都不平行)
dp[i][j * (i - j) + k] = dp[i - j][k] || dp[i][j * (i - j) + k]; //状态转移方程
}
//最后统计得到答案
int ans = 0;
for(int i = 0; i <= n * (n - 1) / 2; i ++)
ans += dp[n][i];
cout << ans << endl;
return 0;
}
数学+递归
dfs搜索:对于 n n n 条边来说,有 n n n 种可能,即有 i i i 条边平行( 1 ≤ i ≤ n 1 \le i \le n 1≤i≤n),当 i = 1 i = 1 i=1 时,说明所有边都不平行且三线无交点;当 i = n i = n i=n 时,说明所有边都平行。可以用搜索树来看(搜索过程相当于一棵树)。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 30, M = 350;
int ans, flag[M];
void solve(int m, int sum) //已有值sum,求m条直线的交点数
{
if(!m) //边界值为m = 0,即所有边都平行
{
if(!flag[sum])
{
ans ++;
flag[sum] = 1;
}
}
for(int i = 0; i < m; i ++) //从所有边都平行到所有边两两不平行且无三线交点
solve(i, sum + i * (m - i));
}
int main()
{
int n;
cin >> n;
solve(n, 0); //sum初始值为0
cout << ans << endl;
return 0;
}
P3913 车的攻击
STL大法好!
这道题的解题思路是:数出所有车所在的不重复的行数和列数,最后用一个公式即可求出结果。
这里得用到 unique 函数,它可以对数组进行去重。并且通过它可以间接地得到数组去重后的不重复元素的个数。
unique(数组名,数组名+大小); (没错和sort几乎一模一样)
然后值得注意的有两点:
第一点:在
u
n
i
q
u
e
unique
unique 之前必须保证去重数组有序,也就是得
s
o
r
t
sort
sort 一下。
第二点:
u
n
i
q
u
e
unique
unique 并不会生成一个新的数组,而是将原数组多余的部分“移”到了数组之后,同时本身还会返回一个指针,指向去重之后的第一个重复元素的下标。
第三点: u n i q u e unique unique 的作用是“去掉”容器中 相邻元素 的重复元素。
利用c++可以指针相加减的特点,我们可以通过 unique减去数组指针 来知道去重之后数组的“大小”。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int main()
{
int n, k;
cin >> n >> k;
int row[N], col[N];
for(int i = 0; i < k; i ++)
cin >> row[i] >> col[i];
sort(row, row + k); //先排序再去重
sort(col, col + k);
LL sizer = unique(row, row + k) - row, sizec = unique(col, col + k) - col; //去重后数组的行数和列数
/* 去重的操作可以这样写,但是这里还要把row和col数组写到main外面去,成为全局变量,因为这里数组会越界
LL sizer = 0, sizec = 0;
for(int i = 0; i < k; i ++)
if(row[i] != row[i + 1])
sizer ++;
for(int i = 0; i < k; i ++)
if(col[i] != col[i + 1])
sizec ++;
*/
cout << (sizer + sizec) * n - sizer * sizec << endl; //知道行数和列数后,用公式即可求出结果
//其实还可以用公式:n * n - (n - sizer) * (n - sizec)
return 0;
}
P1317 低洼地
看大佬博客时,不小心刷到的水题,顺便写在这吧。
思路是:由于是从前往后顺序遍历,对于每个点,我们只需要考虑当前点的高度与它前一个点的高度是否构成下降,我们用一个 f l a g flag flag 来记录是否存在下降,为0则不存在,为1则存在。如果之前存在下降,当前点与前一个点又构成上升,则构成一个低洼地。如果两者高度相等,不做处理直接继续循环。
也就是说,我们直接看有没有下降,如果没有就一直循环直到有;如果有下降,就看有没有对应的上升,如果没有就一直循环到有;如果有上升,就构成一个低洼地。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int main()
{
int n;
cin >> n;
int flag = 0, pre = 0, ans = 0;//flag记录是否存在下降,pre存储上一个点的高度(初始化为0),ans为低洼地的数量
while(n --)
{
int h;
cin >> h;
if(pre < h && flag) //如果之前存在下降,且这里存在上升,则为低洼地
{
ans ++;
flag = 0; //记得更新
}
if(pre > h) //存在下降,更新flag为1
flag = 1;
pre = h; //每次都得记录,下次要使用
}
cout << ans << endl;
return 0;
}
P2638 安全系统
啊!!!我把题意理解错了,导致我肝了一个下午,试了三种方法,全错。真是血的教训啊!
题目说的是一个区间最多能存储两类信号而不是两个信号!!!而这道题恰巧只有两种信号,所以随便塞!
建议可以看一下大佬 x4Cx58x54 的博客题解。
问题重述:
有两种球,分别是黑球(信号 0)和 红球(信号 1),相同类别的球之间没有区别。现在有 n n n 个各不相同的盒子(储存区),要把 a a a 个黑球和 b b b 个红球放进这些盒子里。求方案总数。
每个盒子可以装任意多球,也可以不装。并且以上 a + b a + b a+b 个球不需要全部放进盒子里,甚至可以不放任何球进盒子里。
这里的数据要开 unsigned long long
数据类型。
dfs+记忆化搜索
记此时有 n n n 个盒子,必须放进 i i i 个信号0和 j j j 个信号1的方案数为 f [ n ] [ i ] [ j ] f[n][i][j] f[n][i][j] 。其中 0 ≤ i ≤ a , 0 ≤ j ≤ b 0 \le i \le a, 0 \le j \le b 0≤i≤a,0≤j≤b 。
那么所求的答案为: a n s = ∑ i = 0 a ∑ j = 0 b f [ n ] [ i ] [ j ] ans = \sum \limits _{i = 0} ^{a} \sum \limits _{j = 0} ^{b} f[n][i][j] ans=i=0∑aj=0∑bf[n][i][j] ,
即一个二重循环即可搞定。
那么现在我们需要知道这个 f [ n ] [ i ] [ j ] f[n][i][j] f[n][i][j] 怎么算。这是放 i i i 个黑球,再放 j j j 个红球的方案数,这两步操作是独立的,也就是:
f [ n ] [ i ] [ j ] = g [ n ] [ i ] × g [ n ] [ j ] f[n][i][j] = g[n][i] \times g[n][j] f[n][i][j]=g[n][i]×g[n][j] ,
其中 g [ n ] [ k ] g[n][k] g[n][k] 表示的是 n n n 个盒子中放入 k k k 个同类球的方案数。
那么我们就只需要知道如何计算 g [ n ] [ k ] g[n][k] g[n][k] 的值。具体可见上面大佬的博客所述的 隔板法 。
对了,这里说明一下:
C
n
−
1
k
C _{n - 1} ^k
Cn−1k 与
C
i
+
n
−
1
n
−
1
C _{i + n - 1} ^{n - 1}
Ci+n−1n−1 的区别。前者(插空法)保证了每个区间都有球,而后者每个区间可以没有球。
$$
\begin{equation}
\begin{aligned}
ans
&= \sum \limits _{i = 0} ^{a} \sum \limits _{j = 0} ^{b} f[n][i][j] \
&= \sum \limits _{i = 0} ^{a} \sum \limits _{j = 0} ^{b} g[n][i] \times g[n][j] \
&= \sum \limits _{i = 0} ^{a} g[n][i] \times \sum \limits _{j = 0} ^{b} g[n][j] \
&= \sum \limits _{i = 0} ^{a} C _{i + n - 1} ^{n - 1} \times \sum \limits _{j = 0} ^{b} C _{j + n - 1} ^{n - 1}
\end{aligned}
\end{equation}
$$
再来看一下数据范围。这里的组合数需要算到多大呢?容易看出最大总元素数(就是
C
C
C 的那个下标)是:
n + m a x ( a , b ) − 1 n + max(a, b) - 1 n+max(a,b)−1 。根据题给范围,我们最多需要算到 C 49 k C _{49} ^{k} C49k .
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef unsigned long long ULL;
const int N = 60;
ULL f[N][N][N];
ULL dfs(int num, int x, int y) //有num个存储区间,x个信号0,y个信号1,的方案总数(不一定全部选完)
{
if(x < 0 || y < 0) //边界条件
return 0;
if(f[num][x][y]) //记忆化搜索
return f[num][x][y];
if(!num) //边界条件
return 1;
ULL sum = 0;
for(int i = 0; i <= x; i ++) //遍历所有的可能情况
for(int j = 0; j <= y; j ++)
sum += dfs(num - 1, x - i, y - j);
return f[num][x][y] = sum;
}
int main()
{
int n, a, b;
cin >> n >> a >> b;
cout << dfs(n, a, b) << endl;
return 0;
}
动态规划递推
本质上与第一种方法相同。只不过递推的方式不同,第一种是自顶向下递归,再回溯;第二种是自底向上递推。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef unsigned long long ULL;
const int N = 60;
ULL f[N][N][N]; //f[n][i][j]表示有num个存储区间,x个信号0,y个信号1,的方案总数(不一定全部选完)
int main()
{
int n, a, b;
cin >> n >> a >> b;
//初始化数组,类似于第一种方法的递归边界
for(int i = 0; i <= a; i ++)
for(int j = 0; j <= b; j ++)
f[0][i][j] = 1;
//五层循环递推
for(int i = 1; i <= n; i ++) //枚举存储区间
for(int j = 0; j <= a; j ++) //枚举信号0
for(int k = 0; k <= b; k ++) //枚举信号1
for(int p = 0; p <= j; p ++) //枚举所在存储区间放置信号0的数目
for(int q = 0; q <= k; q ++) //枚举所在存储区间放置信号1的数目
f[i][j][k] += f[i - 1][j - p][k - q]; //递推公式
cout << f[n][a][b] << endl;
return 0;
}
组合数打表+数学技巧
由题意可知:两种信号的选择互不干扰,所以我们可以求出信号0的方案总数和信号1的方案总数,将两者相乘即可得到最终结果。
为了便于求方案数,我们需要预先递推求出组合数。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef unsigned long long ULL;
const int N = 60;
ULL C[N][N];
int main()
{
int n, a, b;
cin >> n >> a >> b;
//预先求组合数备用
for(int i = 0; i <= 50; i ++)
{
C[i][0] = 1;
for(int j = 1; j <= i; j ++)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
//分别计算单独装信号0和信号一的两个总方案数
ULL suma = 0, sumb = 0;
for(int i = 0; i <= a; i ++)
suma += C[n + i - 1][n - 1]; //隔板法技巧
for(int j = 0; j <= b; j ++)
sumb += C[n + j - 1][n - 1];
cout << sumb * suma << endl;
return 0;
}
P1246 编码
来自大佬 Alex_Wei 的博客题解。
具体思路见上方大佬举的例子你就明白了~~(其实是有点繁琐不想手打了)~~。
然后,需要提醒一下的是:先判断该单词是否存在,再一位一位地去考虑共有多少比给出单词小的单词。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef unsigned long long ULL;
const int N = 30;
ULL C[N][N];
int main()
{
string s;
cin >> s;
//这里得特判,题目所描述的非法情形
for(int i = 0; i < s.length() - 1; i ++)
if(s[i] >= s[i + 1])
{
cout << 0 << endl;
return 0;
}
//预先求组合数备用
for(int i = 0; i <= 26; i ++) //不会超过26,后面最多会用到从26个里面选i个的情况
{
C[i][0] = 1;
for(int j = 1; j <= i; j ++)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
//如果输入的字符串的长度为n,则长度从1到n - 1的字符串都排在它前面
ULL ans = 0;
for(int i = 1; i < s.length(); i ++)
ans += C[26][i];
int pos = 0; //pos记录遍历字符串时的某个字符的下标,从左到右遍历
while(s[pos]) //不为空字符
{
char temp = s[pos]; //预先定义变量,方便使用
//初始化每次循环的开头字符,当遍历第一个字符时需特殊赋值
char ch;
if(pos != 0)
ch = s[pos - 1] + 1;
else
ch = 'a';
//在这之前的字符已经确定,此时考虑这个位置上可能的字符,确定后,计算剩余字符的可能组合数
for( ; ch < temp; ch ++)
ans += C[122 - ch][s.length() - pos - 1];
pos ++;
}
ans ++; //自己没算,得加上
cout << ans << endl;
return 0;
}
来看这道题的第二种解法。
受教于 ICE_Wol 的博客题解。
我们可以用 f [ i ] [ j ] f[i][j] f[i][j] 表示以 i i i 为开头的 j j j 位数一共有多少个组合。其中 i i i 是用 1 1 1 到 26 26 26 范围内的数,分别表示 a a a 到 z z z 26个字符。
我们可以找到递推公式,再加上初始化,得到一张表。
在该表格中,横行表示开头字母,纵行表示字串长度,表格中的数据以该字母开头的该长度字符串的总数。对于任何一群以 i i i 开头,长度为 j j j 的字符串,它的数量均为可以以这样一个公式表示:
f [ i ] [ j ] = f [ i + 1 ] [ j − 1 ] + f [ i + 2 ] [ j − 1 ] + ⋯ + f [ 26 ] [ j − 1 ] f[i][j] = f[i + 1][j - 1] + f[i + 2][j - 1] + \cdots + f[26][j - 1] f[i][j]=f[i+1][j−1]+f[i+2][j−1]+⋯+f[26][j−1]
我们可以由这个式子得到 递推公式 : f [ i ] [ j ] = f [ i + 1 ] [ j ] + f [ i + 1 ] [ j − 1 ] f[i][j] = f[i + 1][j] + f[i + 1][ j - 1] f[i][j]=f[i+1][j]+f[i+1][j−1] 。
我们可以利用这个递推公式求出那张表。另外,顺便说一下,打出来的表其实是杨辉三角的一种旋转形态。
最后求结果,某个字符串的编号,就是字符串每一位上的字符在表中对应行从a到该字符的值的和。
a 这里没搞懂!!!
例如:对于一个字符串“ade”,从右至左,第一位是e,则第一行将a-e的数值相加;第二位是d,将第二行a-d的数值相加;以此类推,得到最终答案399。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef unsigned long long ULL;
const int N = 30;
ULL f[N][N];
int main()
{
string s;
cin >> s;
int len = s.length();
//这里得特判,题目所描述的非法情形
for(int i = 1; i < len; i ++)
if(s[i - 1] >= s[i])
{
cout << 0 << endl;
return 0;
}
//初始化
for(int i = 1; i <= 26; i ++)
f[i][1] = 1;
//利用递推公式求表
for(int i = 2; i <= len; i ++)
for(int j = 27 - i; j >= 0; j --)
f[j][i] = f[j + 1][i - 1] + f[j + 1][i];
//最后计算结果
ULL ans = 0;
for(int i = 0; i < len; i ++)
for(int j = 1; j <= s[i] - 'a' + 1; j ++)
ans += f[j][len - i];
cout << ans << endl;
return 0;
}
P2926 [USACO08DEC]Patting Heads S
问题重述:给你 n n n 个数,让你判断对于每个数 k k k 而言,其他 n − 1 n - 1 n−1 个数有多少个可以整除它(即是数字 k k k 的因数)。
初始暴力不优化代码
直接两层循环,第一层枚举每个奶牛的数字,第二层对于每个奶牛的数字从小到大遍历所有奶牛的数字(需要事先排好序),能整除就 a n s + + ans ++ ans++ ,第二层遍历完输出即可。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e5 + 10;
int main()
{
int n;
cin >> n;
int a[N], b[N]; //a数组记录原始数据,b数组记录排序后的数据
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + n + 1); //排序
for(int i = 1; i <= n; i ++)
{
int ans = 0;
for(int j = 1; b[j] <= a[i] && j <= n; j ++) //因数最大不能超过自己本身
if(a[i] % b[j] == 0)
ans ++;
cout << -- ans << endl; //因为把自己也算进去了
}
return 0;
}
看题解后自写的筛法(78分)(13个点 T L E TLE TLE 3个点)
从1~n挨个开始,将数组中的数的每个倍数都加上1,相当于筛子把对应的数都进行相应操作。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int cnt[M];
int main()
{
int n;
cin >> n;
int cow[N]; //cow数组记录原始数据
for(int i = 1; i <= n; i ++)
cin >> cow[i];
for(int i = 1; i <= n; i ++)
for(int j = 1; j * cow[i] < M; j ++) //不要超过极限值,以cow[i]为筛子,乘以倍数去筛数
cnt[j * cow[i]] ++;
for(int i = 1; i <= n; i ++)
cout << cnt[cow[i]] - 1 << endl;
return 0;
}
同样的方法,自写了一点小优化,结果(86分)(13个点 T L E TLE TLE 2个点)
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int cnt[M];
int main()
{
int n;
cin >> n;
int cow[N], max_cow = 0; //cow数组记录原始数据,max_cow记录最大的数
for(int i = 1; i <= n; i ++)
{
cin >> cow[i];
max_cow = max(max_cow, cow[i]);
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j * cow[i] <= max_cow; j ++) //同上,只是边界缩小了
cnt[j * cow[i]] ++;
for(int i = 1; i <= n; i ++)
cout << cnt[cow[i]] - 1 << endl;
return 0;
}
妈的,终于过了。在前面(86分)的基础上加了一点小优化。
和之前的区别在于:因为每一个数有重复的,所以在找倍数的时候可以加上一个小的优化,不要每次加一,就一次性加上重复出现的次数就好了。
这个是遍历所有的数字,就会保证每个数字只会被遍历一遍。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int cnt[M], num[M]; //cnt[i]记录数字i在所有奶牛数字中的因数的总数,num[i]记录数字i出现的次数
int main()
{
int n;
cin >> n;
int cow[N], max_cow = 0; //cow数组记录原始数据,max_cow是奶牛中数字最大的那个
for(int i = 1; i <= n; i ++)
{
cin >> cow[i];
num[cow[i]] ++;
max_cow = max(max_cow, cow[i]); //求数字最大数
}
for(int i = 1; i <= max_cow; i ++) //遍历所有范围内的数字,这里和之前不一样
{
if(!num[i]) //不存在就撤
continue;
for(int j = 1; i <= max_cow / j; j ++) //只需要小于数字最大的那个即可
cnt[j * i] += num[i];
}
for(int i = 1; i <= n; i ++) //记得要减去自身
cout << cnt[cow[i]] - 1 << endl;
return 0;
}
这里还有一种方法,也可以避免重复计算导致的超时。这里的第一层循环是遍历奶牛数,他通过使遍历完的数字对应出现的次数为0,让每个数字只遍历一遍(下次直接跳过),从而节省时间。
当数据范围为 1 0 5 10^5 105 级别时, s c a n f scanf scanf 和 p r i n t f printf printf 比 c i n cin cin 和 c o u t cout cout 快两倍左右。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int cnt[M], num[M]; //cnt[i]记录数字i在所有奶牛数字中的因数的总数,num[i]记录数字i出现的次数
int main()
{
int n;
cin >> n;
int cow[N], max_cow = 0; //cow数组记录原始数据,max_cow是奶牛中数字最大的那个
for(int i = 1; i <= n; i ++)
{
cin >> cow[i];
num[cow[i]] ++;
max_cow = max(max_cow, cow[i]); //求数字最大数
}
for(int i = 1; i <= n; i ++) //遍历所有奶牛,这里和之前更不一样
{
if(!num[cow[i]]) //不存在就撤
continue;
for(int j = 1; cow[i] <= max_cow / j; j ++) //只需要小于数字最大的那个即可
cnt[j * cow[i]] += num[cow[i]];
num[cow[i]] = 0; //这里是关键,使得每个数字只会被遍历一遍
}
for(int i = 1; i <= n; i ++) //记得要减去自身
cout << cnt[cow[i]] - 1 << endl;
return 0;
}
这道题其实还有另外一种优化方法。直接枚举因数,时间复杂度为 n l o g n nlogn nlogn 。具体见 题解 ,这里实在太多了,就不写了。
P3383 【模板】线性筛素数
此题是线性筛(欧拉筛)的模板题。
可以参考大佬 学委 的博客题解。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e7 + 10, M = 1e8 + 10; //这里的N必须要足够大才行,1e6不够会报错
//N表示素数的个数,M表示在多大范围内求素数
bool flag[M]; //标记是否为素数,为true则不是素数,为false则是素数
int main()
{
int n, q;
cin >> n >> q;
int cnt = 1, primes[N];
for(int i = 2; i <= n; i ++)
{
if(!flag[i]) //经过前面的筛选,为false,说明是素数
primes[cnt ++] = i;
//这里是筛选素数最关键也是最核心的一部分,它保证了每个合数被其最小素因数给筛掉
for(int j = 1; primes[j] <= n / i; j ++) //这里不用写j < cnt,因为当j最大到达cnt - 1时,一定会被break掉
{
flag[i * primes[j]] = true;
if(i % primes[j] == 0) //当满足条件时必须break掉,否则不符合线性的要求,比如2和10,不停止的话,会筛掉30
break;
}
}
while(q --) //所有素数都求出后,查询直接输出即可
{
int k;
cin >> k;
cout << primes[k] << endl;
}
return 0;
}
P1835 素数密度
这里有两个(91分)的代码,11个数据
T
L
E
TLE
TLE 1个数据。可能数据范围搞太大了。但是,我发现,开
O
2
O_2
O2 优化居然可以过。还有,更不可思议的是:把数据类型开成 register unsigned int
真的可以过。
思路是用线性筛法预先求出所有可能会用到的素数(尽管数据 n n n 可能会很大,但是我们只用 n \sqrt n n 以内的素数就可以将他们筛掉了)。然后遍历区间所有数,对于每个数,让它除以从小到大的已经求出的素数,如果出现可以整除的情况就标记。总之遍历完了之后就可以根据标记求出所有的素数个数。这里是朴素的筛法。
等等,经过一个上午的调试,我发现只需要开一个 unsigned int
数据类型即可。
这里有一个恐怖的数据:
输入数据:2146483647 2147483647
输出数据:46603
看了这个数据,相信你一定知道必须得开 unsigned int
数据类型。
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 5e4 + 10;
bool flag[N];
int main()
{
LL L, R;
cin >> L >> R;
if(L == 1)
L = 2;
int cnt = 0, primes[N];
for(int i = 2; i <= R / i; i ++)
{
if(!flag[i])
primes[++ cnt] = i;
for(int j = 1; i * primes[j] <= R / (i * primes[j]); j ++)
{
flag[i * primes[j]] = true;
if(i % primes[j] == 0)
break;
}
}
int ans = 0;
for(int i = L; i <= R; i ++) //遍历区间内每个数 //把i的数据类型改为unsigned int就可以过了
{
int sign = 1;
for(int j = 1; j <= cnt && primes[j] * primes[j] <= i; j ++) //乘法比除法要快,这里不可写成除法(超时)
if(i % primes[j] == 0) //满足条件则为合数,标记后直接break掉
{
sign = 0;
break;
}
ans += sign;//if(sign)cout << i << endl;
}
//for(int i = 1; i <= cnt; i ++)cout << primes[i] << endl;
cout << ans << endl;
return 0;
}
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 5e4 + 10, M = 1e6 + 10;
bool flag[N];
int main()
{
ios::sync_with_stdio(false); //这个没啥用,可以不用管
LL L, R;
cin >> L >> R;
if(L == 1)
L = 2;
int cnt = 0, primes[N];
for(int i = 2; i <= R / i; i ++)
{
if(!flag[i])
primes[++ cnt] = i;
for(int j = 1; i * primes[j] <= R / (i * primes[j]); j ++)
{
flag[i * primes[j]] = true;
if(i % primes[j] == 0)
break;
}
}
int ans = R - L + 1, sign[M];
memset(sign, 0, sizeof sign);
for(int i = L; i <= R; i ++) //把i的数据类型改为unsigned int就可以过了
for(int j = 1; primes[j] * primes[j] <= i && j <= cnt; j ++)
if(i % primes[j] == 0)
{
sign[i - L] = 1;
break;
}
for(int i = 0; i <= R - L; i ++)
ans -= sign[i];
cout << ans << endl;
return 0;
}
下面这是别人的代码:网址
它开了 register unsigned int
的数据类型,所以过了。
#include<bits/stdc++.h>
using namespace std;
bool b[80000],e;
const int N=50000;
unsigned int a[80000],l,r,k,s;
int main(){
cin>>l>>r;
for(register unsigned int i=2;i<=(N>>1);++i)
if(!b[i])
for(register unsigned int j=(i<<1);j<=N;j+=i)
b[j]=1;
for(register unsigned int i=3;i<=N;++i)
if(!b[i])a[++k]=i;
for(register unsigned int i=l;i<=r;++i){
if(i==2){++s;continue;}
if(i<2||(i&1)==0)continue; //2的倍数直接判断下一个
e=1;
for(register unsigned int j=1;a[j]*a[j]<=i;++j){ //这里可以不用加j <= k,因为素数平方超过i时会自动结束
if(i%a[j]==0){ //而我自己写的那个素数平方不会超过i,所以会越界
e=0;
break;
}
}
s+=e;
}
cout<<s<<endl;
}
//我修改后的它的代码
#include<bits/stdc++.h>
using namespace std;
bool b[N],e;
const int N=50000;
unsigned int a[N],l,r,k,s;
int main(){
cin>>l>>r;
for(unsigned int i=2;i<=N;++i)
if(!b[i])
for(unsigned int j=2*i;j<=N;j+=i)
b[j]=1;
for(unsigned int i=2;i<=N;++i)
if(!b[i])a[++k]=i;
for(unsigned int i=l;i<=r;++i){
//if(i==2){++s;continue;}
if(i<2)continue;
e=1;
for(unsigned int j=1;j <= k && a[j]*a[j]<=i;++j){
if(i%a[j]==0){
e=0;
break;
}
}
s+=e;
}
cout<<s<<endl;
}
满分代码:
求素数的方法都一样,都是线性筛法。区别在于用已求得的素数筛掉区间内合数得的方法。这里用的是埃氏筛的思想。遍历所有已求得的素数,利用这个素数去筛掉所有可以被它整除的合数,同时用数组给每个区间内的合数做标记。最后遍历所有的标记,得到结果。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 5e4 + 10, M = 1e6 + 10;
bool flag[N];
int sign[M];
int main()
{
int L, R;
cin >> L >> R;
if(L == 1) //这里得特判L为1的情况
L = 2;
//线性筛法求素数
int cnt = 0, primes[N];
for(int i = 2; i <= R / i; i ++)
{
if(!flag[i])
primes[++ cnt] = i;
for(int j = 1; i * primes[j] <= R / (i * primes[j]); j ++)
{
flag[i * primes[j]] = true;
if(i % primes[j] == 0)
break;
}
}
//埃氏筛法思想
for(int i = 1; i <= cnt; i ++) //遍历每个素数,利用素数筛掉区间内的合数
for(LL j = max(2 * primes[i], ((L - 1) / primes[i] + 1) * primes[i]); j <= R; j += primes[i]) //j每次都从大于等于L且最靠近L的可被primes[i]整除的数开始;
//这里j还可以取(l + primes[i] - 1) / primes[i] * primes[i],比如[14, 21),通过它可以将14与[15, 20]分离开
sign[j - L] = 1; //记下合数
//注意,这里的j必须要开long long,否则会超过int的范围,因为j可取L + primes[i],同时这里的j保证了大于等于L
int ans = R - L + 1;
for(int i = 0; i <= R - L; i ++) //合数的sign值为1,故减去它们
ans -= sign[i];
cout << ans << endl;
return 0;
}
P1029 [NOIP2001 普及组] 最大公约数和最小公倍数问题
两个数的乘积等于其最大公约数 x x x 与最小公倍数 y y y 的乘积。
思路是找到的两个数要满足这三个条件中的任意两个(知道任意两个可求出第三个)即可:最大公约数为 x x x,最小公倍数为 y y y,二者乘积等于 x × y x \times y x×y 。
我们从最小的数
x
x
x 开始枚举,每次都 i += x
,对于每个数
i
i
i ,求出其对应的那个数 x * y / i
,判断它是否满足最大公约数等于
x
x
x,若满足,则找到了,
a
n
s
+
+
ans ++
ans++ ,不满足就判断下一个数。这里得特判完全平方数的情况,只能算一个。最后输出结果要乘以2,再减去完全平方数的情况即可。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
LL gcd(LL a, LL b)
{
if(!b)
return a;
return gcd(b, a % b);
}
int main()
{
LL x, y;
cin >> x >> y;
int ans = 0, flag = 0; //ans记录答案,flag记录是否存在完全平方数
for(LL i = x; i * i <= x * y; i += x)
{
if(x * y % i == 0 && gcd(x * y / i, i) == x) //这里不要忘了判断是否能整除的情况
{
ans ++;
if(i * i == x * y)
flag = 1;
}
}
cout << 2 * ans - flag << endl; //输出答案
return 0;
}
P1072 [NOIP2009 提高组] Hankson 的趣味题
感谢大佬zzlzk的题解分析。
对于两个正整数 a a a, b b b,设 g c d ( a , b ) = k gcd(a,b)=k gcd(a,b)=k,则存在 g c d ( a / k , b / k ) = 1 gcd(a/k,b/k)=1 gcd(a/k,b/k)=1。
这道题需满足的条件是: g c d ( x / a 1 , a 0 / a 1 ) = 1 gcd(x/a_1, a_0/a_1) = 1 gcd(x/a1,a0/a1)=1 和 g c d ( b 1 / b 0 , b 1 / x ) = 1 gcd(b_1/b_0, b_1/x) = 1 gcd(b1/b0,b1/x)=1 。
b 1 \sqrt {b_1} b1 枚举的 b 1 b_1 b1 因子(也就是 x x x ),如果这个数是 a 1 a_1 a1 的整数倍并且满足那两个式子,则 a n s + + ans ++ ans++ 。
首先奉上我的错误代码:
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 4e4;
int gcd(int a, int b) //求最大公约数
{
if(!b)
return a;
return gcd(b, a % b);
}
int main()
{
int n;
cin >> n;
while(n --)
{
int a0, a1, b0, b1;
cin >> a0 >> a1 >> b0 >> b1;
//代码段一
int factors[N], cnt = 0; //factors记录满足条件(可整除b1且可被a1整除)的因子,cnt记录因子数
for(int i = a1; i * i <= b1; i += a1) //因为要满足是a1的倍数,所以i += a1
if(b1 % i == 0) //且可整除b1
factors[cnt ++] = i;
for(int i = cnt - 1; i >= 0; i --) //寻找每个因子的另一半
if(factors[i] * factors[i] != b1 && (b1 / factors[i]) % a1 == 0) //不为平方数且可被a1整除
factors[cnt ++] = b1 / factors[i];
int ans = 0; //记录最终结果
for(int i = 0; i < cnt; i ++)
if(gcd(factors[i] / a1, a0 / a1) == 1 && gcd(b1 / factors[i], b1 / b0) == 1)
ans ++;
cout << ans << endl;
}
return 0;
}
它错在哪呢?错在漏了情况,在代码段一,当 i i i 不满足情况时,其对应的因子也是有可能满足情况的。但是后来的代码显示一旦代码段一筛选错误了,后面就不会考虑之前漏掉的情况了。
比如对于数据: 21222 2 999993719 1999987438
1不满足情况,但 1999987438 ÷ 1 = 1999987438 1999987438 \div 1 = 1999987438 1999987438÷1=1999987438 满足情况。但一旦1被筛掉了,1999987438也就被筛掉了。
正确代码:
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 4e4;
int gcd(int a, int b) //求最大公约数
{
if(!b)
return a;
return gcd(b, a % b);
}
int main()
{
int n;
cin >> n;
while(n --)
{
int a0, a1, b0, b1;
cin >> a0 >> a1 >> b0 >> b1;
//代码段一
int factors[N], cnt = 0; //factors记录满足条件(可整除b1且可被a1整除)的因子,cnt记录因子数
for(int i = 1; i * i <= b1; i ++) //因为要满足是a1的倍数,所以i += a1
if(b1 % i == 0) //且可整除b1
factors[cnt ++] = i;
for(int i = cnt - 1; i >= 0; i --) //寻找每个因子的另一半
if(factors[i] * factors[i] != b1) //不为平方数且可被a1整除
factors[cnt ++] = b1 / factors[i];
int ans = 0; //记录最终结果
for(int i = 0; i < cnt; i ++)
if(factors[i] % a1 == 0 && gcd(factors[i] / a1, a0 / a1) == 1 && b1 % factors[i] == 0 && gcd(b1 / factors[i], b1 / b0) == 1)
ans ++;
cout << ans << endl;
}
return 0;
}
P1069 [NOIP2009 普及组] 细胞分裂
基本思路:此题考察分解质因数。题目要求最小的正整数 k k k 使得有一个 i i i ,满足 m 1 m 2 ∣ S i k m_1^{m_2}|S_i^k m1m2∣Sik。中间的竖线表示整除。
我们枚举 m 1 m_1 m1 的所有质因数及其指数,存入一个数组 p m 1 [ N ] pm1[N] pm1[N] 。然后将每个质因数的指数都乘以 m 2 m_2 m2 得到 m 1 m 2 m_1^{m_2} m1m2 的质因数及其指数。然后与 S i S_i Si 的质因数及其指数进行比较, S i S_i Si 的质因数集合必须包含 m 1 m 2 m_1^{m_2} m1m2 的质因数集合,不然不可能满足条件,输出 − 1 -1 −1 。之后将 S i S_i Si 的所有质因数指数都乘以最小的正整数 k k k 使得每个对应的质因数的指数都不小于 m 1 m 2 m_1^{m_2} m1m2 的质因数的指数。
据说此题还有 g c d gcd gcd 的解法,见大佬暗ざ之殇博客。
分解质因数的解法见这位大佬刘心远。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int N = 3e4, INF = 2147483647;
int main()
{
int n, m1, m2;
cin >> n >> m1 >> m2;
int pm1[N] = {0}, bound;
for(int i = 2; i <= m1; i ++)
{
while(m1 > 0 && m1 % i == 0)
{
m1 /= i;
pm1[i] ++;
}
pm1[i] *= m2;
if(m1 == 1)
{
bound = i;
break;
}
}
int minm = INF;
while(n --)
{
int s;
cin >> s;
int maxm = 0, flag = 1;
for(int i = 2; i <= bound; i ++)
{
if(pm1[i] && s % i != 0)
{
flag = 0;
break;
}
if(pm1[i] && s % i == 0)
{
int num = 0;
while(s > 0 && s % i == 0)
{
s /= i;
num ++;
}
maxm = max(maxm, (num - 1 + pm1[i]) / num);
}
}
if(flag)
minm = min(minm, maxm);
}
if(minm != INF)
cout << minm << endl;
else
cout << -1 << endl;
return 0;
}
此题第一个数据较坑,如下:
输入:
10
1 1
645855438 311218536 15797250 227733808 68960766 222753465 32576949 223726014 566371728 250463473
输出:0
P1572 计算分数
我实在太菜了,这道题都不会做。Jouna_Kasa_Hasinele
简单的模拟,注意读入可以将负号一起读入,加减都可以不考虑。利用gcd来通分和约分,最后检查一下分母是不是负数就行了。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
int main()
{
int nume, deno, a, b; //numerator和denominator是实时更新的分子和分母,而a和b是输入的分子和分母
scanf("%d/%d", &nume, &deno);
while((scanf("%d/%d", &a, &b)) != EOF) //这里非常关键
{
nume = nume * b + a * deno;
deno *= b;
int g = gcd(nume, deno);
nume /= g;
deno /= g;
}
if(deno < 0) //这里有个坑点,分母可能为0
{
nume = - nume;
deno = - deno;
}
if(deno == 1)
printf("%d\n", nume);
else
printf("%d/%d\n", nume, deno);
return 0;
}
P4057 [Code+#1]晨跑
本题思路很简单:三个数的最小公倍数就是其中两个数的最小公倍数与第三个数的最小公倍数。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
LL gcd(LL a, LL b)
{
return b == 0 ? a : gcd(b, a % b);
}
int main()
{
LL a, b, c;
cin >> a >> b >> c;
LL lcm = a * b / gcd(a, b); //两个数的最小公倍数
cout << lcm * c / gcd(lcm, c) << endl; //三个数的最小公倍数
return 0;
}
P1414 又是毕业季II
基本思路:我们想到,k个数的公约数含义就是这k个数均含有某个因数,如果我们把所有数的因数全部求出来,发现有k个数均含有某个因数,那么这个数必然是这k个数的公约数。其中找出最大的就是它们的最大公约数。
每个数分解因数,
c
[
i
]
c[i]
c[i] 表示
i
i
i 作为因子的次数。 对于答案
i
i
i,
c
[
x
]
>
i
c[x]>i
c[x]>i 的
x
x
x 可以作为答案。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
LL gcd(LL a, LL b)
{
return b == 0 ? a : gcd(b, a % b);
}
int main()
{
int n;
cin >> n;
int cnt[N] = {0}, maxx = 0; //maxx用来记录最大的能力值
for(int i = 1; i <= n; i ++) //用cnt数组记录每个能力值的因数
{
int x;
cin >> x;
maxx = max(maxx, x);
for(int i = 1; i <= x / i; i ++)
if(x % i == 0)
{
cnt[i] ++;
if(i != x / i)
cnt[x / i] ++;
}
}
for(int i = 1, x = maxx; i <= n; i ++) //由于共同公约数的个数越多,其数值就越小,如1是最多的,但是最小的
{
while(cnt[x] < i)
x --;
cout << x << endl;
}
return 0;
}
P3601 签到题
感谢大佬Hello_BABY_OvO的题解。
考虑枚举然后求和,题中的 q i a n d a o ( x ) = x − p h i ( x ) qiandao(x)=x-phi(x) qiandao(x)=x−phi(x) ,先筛出 1 0 6 10^{6} 106 以内所有的质数,然后用埃氏筛的思想去算出每个质数对 [ l , r ] [l, r] [l,r] 区间里每个数的贡献,最后再特判一下大于 r \sqrt{r} r 的质数就OK了!
要记住一个特殊值 ϕ ( 1 ) = 1 \phi(1) = 1 ϕ(1)=1 。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10, mod = 666623333;
int primes[N], cnt;
bool st[N];
void get_primes(int n) //欧拉筛法筛质数
{
for(int i = 2; i <= n; i ++)
{
if(!st[i])
primes[cnt ++] = i;
for(int j = 0; i <= n / primes[j]; j ++)
{
st[i * primes[j]] = true;
if(i % primes[j] == 0)
break;
}
}
}
int main()
{
LL l, r;
cin >> l >> r;
int len = r - l;
get_primes(ceil(sqrt(r)));
LL phi[N], div[N]; //phi记录欧拉函数的值,div记录分解质因数后的值
for(int i = 0; i <= len; i ++)
phi[i] = div[i] = i + l; //初始化,映射到小区间
//求欧拉函数,顺带求出了div分解质因数后的值
for(int i = 0; i < cnt; i ++) //首先枚举所有的质数,用每一个质数去筛区间里的数
{
int p = primes[i];
for(int j = (p - l % p) % p; j <= len; j += p) //然后枚举区间里的数,这里的j有个小技巧
{
phi[j] = phi[j] / p * (p - 1); //更新欧拉函数
while(div[j] % p == 0) //分解质因数
div[j] /= p;
}
}
for(int i = 0; i <= len; i ++) //由于可能存在一个大于根号r的质因数,所以这里得特判
if(div[i] > 1)
phi[i] = phi[i] / div[i] * (div[i] - 1);
LL ans = 0; //计算最后的结果
for(int i = 0; i <= len; i ++)
ans = (ans + i + l - phi[i]) % mod;
cout << ans << endl;
return 0;
}
P2651 添加括号III
首先,先献上一份错误代码。这个错误搞了我很久。原因就是continue在输入数据时,不能乱用,否则会导致数据错乱。continue会导致后面的数据没来得及输入完,就直接跳到下一个循环中,上一次循环为输入的值赋到了这一次。
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
/*
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
*/
int main()
{
int t;
cin >> t;
while(t --)
{
int n;
cin >> n;
int nume, deno;
cin >> nume >> deno;
deno /= __gcd(nume, deno);
if(deno == 1)
{
cout << "Yes" << endl;
continue;
}
n -= 2;
int flag = 0;
while(n --)
{
cin >> nume;
deno /= __gcd(nume, deno);
if(deno == 1)
{
cout << "Yes" << endl;
flag = 1;
break;
}
}
if(!flag)
cout << "No" << endl;
}
return 0;
}
那么怎么修改呢?只需要把没读入的数据读入即可,用一个变量放掉,这个变量的功能只是读入过掉剩余的数。这里使用了一个变量r。
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
/*
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
*/
int main()
{
int t;
cin >> t;
while(t --)
{
int n;
cin >> n;
int nume, deno;
cin >> nume >> deno;
deno /= __gcd(nume, deno);
if(deno == 1)
{
cout << "Yes" << endl;
int r;
n -= 2; //记得要减2
while(n --)
cin >> r;
continue;
}
n -= 2;
int flag = 0;
while(n --)
{
cin >> nume;
deno /= __gcd(nume, deno);
if(deno == 1)
{
cout << "Yes" << endl;
flag = 1;
break;
}
}
if(!flag)
cout << "No" << endl;
int r; //读入并过掉数据
if(n > 0)
{
while(n --)
cin >> r;
}
}
return 0;
}
a1肯定是分子,a2肯定是分母,那么尽可能多的是a3以后的变为分子,怎么办呢?很简单
a1/(a2/a3/a4/…)=a1a3a4…/a2
所以我们只要确认a1a3a4…/a2是否是整数。如果进行约分,知道a2能被约分成1,那么就是整数。
每次将a2=a2/gcd(a2,ai),i=(1,3,4,5…),即可约分。
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 1e4 + 10;
/*
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
*/
int main()
{
int t;
cin >> t;
while(t --)
{
int n;
cin >> n;
int a[N];
for(int i = 1; i <= n; i ++) //先读入所有数据
cin >> a[i];
for(int i = 1; i <= n; i ++) //然后计算将第二个数的公因数一个一个去掉
{
if(i == 2)
continue;
a[2] /= __gcd(a[2], a[i]);
}
if(a[2] == 1) //判断是否为1
cout << "Yes" << endl;
else
cout << "No" << endl;
}
return 0;
}
P2660 zzc 种田
一个数学迭代,类似于GCD。
每次都切一个边长为 m i n ( x , y ) min(x,y) min(x,y) 的正方形,每次不要一个正方形一个正方形的切,可以一次切多个,也就是切 m a x ( x , y ) / m i n ( x , y ) max(x,y)/min(x,y) max(x,y)/min(x,y) 个。
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
int main()
{
LL x, y;
cin >> x >> y;
if(y > x) //每次都保证x >= y,其实这段if代码可以删掉,也能过
swap(x, y);
LL ans = 0;
while(y) //所以只要判断y是否为0即可
{
ans += x / y * y;
x %= y;
swap(x, y);
}
cout << ans * 4 << endl;
return 0;
}
P1403 [AHOI2005]约数研究
本人的思路是:利用 n \sqrt {n} n 以内的数去更新 n n n 以内的约数个数。但是得注意一个数 i i i ,只能去更新大于等于 i 2 i^2 i2 的数,因为这里保证了,每个数 j j j 只被小于等于 j \sqrt {j} j 的数更新一次。这样才能降低时间复杂度。
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int f[N];
int main()
{
int n;
cin >> n;
for(int i = 1; i * i <= n; i ++)
for(int j = i * i; j <= n; j += i) //这里要特别注意j的初始值
if(i * i == j)
f[j] ++;
else
f[j] += 2;
int ans = 0; //累加得结果
for(int i = 1; i <= n; i ++)
ans += f[i];
cout << ans << endl;
return 0;
}
然而我发现了更精妙的做法:见博客:Kelin
#include <bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
int main()
{
int n;
cin >> n;
int ans = 0;
for(int i = 1, j; i <= n; i = j + 1)
{
j = n / (n / i); //这里的i和j的取值要好好探讨
ans += n / i * (j - i + 1);
}
cout << ans << endl;
return 0;
}
另外,这里还有一道类似的贼难的题:SP26073 DIVCNT1 - Counting Divisors 。
P1593 因子和
我太菜了,不想解释。就说一点:这道题用到了分解质因数、快速幂、费马小定理(求逆元),等比数列求和等知识。
具体见大佬们的博客吧,被这道题给搞抑郁了。
#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10, mod = 9901;
int qpower(int a, int k, int p) //为了防止运算过程中溢出,最好多用取模运算
{
int res = 1;
while(k)
{
if(k & 1)
res = (LL)(res * a) % p;
a = (a * a) % p;
k >>= 1;
}
return res;
}
int main()
{
int a, b;
cin >> a >> b;
int fac[N], power[N] = {0}, cnt = 0;
for(int i = 2; i <= a / i; i ++)
{
if(a % i == 0)
{
fac[cnt] = i;
while(a % i == 0)
{
a /= i;
power[cnt] ++;
}
power[cnt] *= b;
cnt ++;
}
}
if(a > 1)
{
fac[cnt] = a;
power[cnt ++] = b;
}
int ans = 1;
for(int i = 0; i < cnt; i ++)
{
if(fac[i] % mod == 1)
ans = (ans * ((power[i] + 1) % mod)) % mod;
else
ans = ans * (qpower(fac[i] % mod, power[i] + 1, mod) - 1) % mod * qpower((fac[i] - 1) % mod, mod - 2, mod) % mod; //这一行相当重要,尤其是取模的地方特别讲究
}
cout << (ans % mod + mod) % mod << endl; //防止出现负数,但是为什么会出现负数不得而知
return 0;
}