NewCoder Weekly Contest 46
NewCoder Weekly Contest 46
C 爱音开灯 简单数论
Problem Statement
有无穷个灯排成一排,编号为从 1 开始,初始时所有灯都是关闭的。改变第 \(i\) 个灯的开闭状态会同时改变所有编号为 \(i\) 倍数的灯的开闭状态。
Anon会从 1 到 \(n\) ,依次改变每一个灯的开闭状态,她想知道第 \(x\) 个灯最终的状态是什么?如果灯是关闭的,输出 "OFF" ,否则输出 "ON" 。
Input
输入两个正整数 \(n,x(1 \leq n,x \leq 10^{12})\) 。
Output
输出一个字符串表示答案。如果灯是关闭的,输出 "OFF" ,否则输出 "ON" 。
Sample
1 1
输出
ON
说明
Anon开第 1 个灯时,第 1 个灯从关闭到开启;
答案为 "ON" 。
Solution
观察题目,先想暴力做法,既然第 \(i\) 个灯的开闭状态会同时改变所有编号为 \(i\) 倍数的灯的开闭状态,那我们就枚举1~n盏灯,如果i是x的因子,就改变x的开闭状态,可以通过50%的数据,IOI赛制下也能拿分
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, k;
void solve()
{
cin >> n >> k;
bool flag = false;
for (int i = 1; i <= n; i ++ )
{
if (k % i == 0) flag -= 2 * flag;
}
if (flag = true) cout << "ON" << endl;
else cout << "OFF" << endl;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
优化代码
思路: 带限制的因子数求解
观察题目,第 \(i\) 个灯的开闭状态会同时改变所有编号为 \(i\) 倍数的灯的开闭状态,所以第x个灯的开闭状态只会被i是x的因数的灯所影响,而由数论部分知识(试除法分解质因数)我们知道,x的因数一定是成对出现的,若i
是x
的因子,那么x / i
也是x
的因子,所以有\(其中一个x的因子 <= \sqrt{x}\),所以我们只需要枚举这个较小的x的因子,利用它来求较大的因子是否满足情况,这样可以大大降低循环次数,从而降低时间复杂度,只需要枚举\(\sqrt{x}\)次,使得时间复杂度为\(O(\sqrt{x})\),x上限是\(10^{12}\),最多循环\(\sqrt{10^{12}}=10^{6}\)
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, x;
int sum; //记录第x盏灯最终需要改变多少次
void solve()
{
cin >> n >> x;
for (int i = 1; i <= x / i; i ++ ) //只用对x的其中一个因子枚举,这个因子一定<=sqrt(x),这样可以将时间复杂度降到O(sqrt(n))级,就不会超时
{
if (x % i == 0) //如果i是x的因子
{
if (i <= n) //同时第i盏灯在操作的范围内
{
sum ++ ;
if ((x / i <= n) && (x / i != i)) //判断另一个因子x / i 是否会被点亮 以及是否和i相等
{
sum ++ ;
}
}
}
}
if (sum % 2 == 0) cout << "OFF" << endl; //如果操作次数是偶数 最终灯还是没开
else cout << "ON" << endl;
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
D 小灯做题 BFS
Problem Statement
Tomori在做一个题目,这个题目初始有四个非负整数 \(a,b,c,k\) 。
每次Tomori可以对 \(a, b, c\) 进行操作:选择其中两个数 \(x,y\),然后将剩下一个数改成 \(\operatorname{mex}(x,y)\)。
Tomori想知道她最少操作多少次,能让 \(a,b,c\) 中的一个数变成 \(k\)。若无解,则输出 \(-1\)。
\(\operatorname{mex}(x,y)\) 表示最小的非负整数 \(p\) ,满足 \(p\ne x\) 且 \(p\ne y\) ,例如 \(\operatorname{mex}(1,2)=0,\operatorname{mex}(0,2)=1\) 。
Input
第一行输入一个正整数 \(T(1 \leq T \leq 100)\) ,表示询问次数。
接下来 \(T\) 行,每行输入四个非负整数 \(a,b,c,k(0 \leq a,b,c,k \leq 10^9)\) ,表示询问。
Output
对于每个询问,在一行中输出一个整数表示答案。
Sample
输入
3
1 2 3 1
1 2 3 0
1 2 3 4
输出
0
1
-1
说明
第 1 个询问:
第 1 个数已经是1了;
不需要进行操作。
第 2 个询问:
选择第 1、2 个数,将第 3 个数变成 mex(1,2) = 0 ;
只需要进行 1 次操作。
Solution
题目告诉我们要在a b c中选两个数进行\(mex(x, y)\)操作,使得其等于k
\(\operatorname{mex}(x,y)\) 表示最小的非负整数 \(p\),其实可以很容易发现,两个数求出来的\(\operatorname{mex}(x,y)\)只会在0 1 2这三个数中出现
所以𝑘值(𝑘>=3)无法通过mex操作得到,只有刚开始k就等于a或b或c时,才能满足条件,操作次数为0,特殊处理即可
所以我们只需要罗列mex(x, y)分别等于0 1 2的情况
做的时候一直没有AC的原因之一就是mex的情况罗列错了,实际上不用那么麻烦 反正mex算出来的值就0 1 2三种情况,我们从小到大枚举看哪个满足条件就行了
int mex(int a,int b){
for(int i=0;;i++){
if(i!=a&&i!=b)return i;
}
}
接着,我们可以利用BFS,同时对bfs(a,b,mex(a,b),k,dep+1);
bfs(a,mex(a,c),c,k,dep+1);
bfs(mex(b,c),b,c,k,dep+1);
进行搜索,return的条件是if (k == a || k == b || k == c) return cnt;
,搜的时候我们要顺便记录其操作次数dep,同时加上一个避免无限向下递归的条件(做的时候没加导致MLE),if (cnt > 5) return cnt;
,由于k只能从0 1 2中选取,a b c经过几次mex()操作就肯定能得到,如果没有得到k,那么永远也不会得到。操作次数最多的情况就是 a = 3 b = 3 c = 3 k = 2 需操作3次才能得到k
我们就给向下搜索限定一个层数,也就是最高操作次数,限定为5,任取即可,只要使得不会MLE
//自己写的版本
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T;
int a, b, c, k;
int cnt;
//浪费时间的条件判断 容易出错还浪费时间
int mex(int a, int b)
{
if (a != 0 && b != 0)
return 0;
if ((a == 0 && b == 1) || (a == 1 && b == 0))
return 2;
if (a == 0 || b == 0)
return 1;
return -1;
}
int dfs(int a, int b, int c, int cnt)
{
if (cnt > 5) return cnt; //关键点 使得不会无限向下搜索
//麻烦的设置数据,目的是为了全部三种情况返回后,return其中的最小值,看看范式怎么写
int cnt1 = 9999, cnt2 = 9999, cnt3 = 9999;
if (k == a || k == b || k == c) return cnt;
cnt1 = dfs(a, b, mex(a, b), ++ a1);
cnt2 = dfs(a, mex(a, c), c, ++ b1);
cnt3 = dfs(mex(b, c), b, c, ++ c1);
return min(cnt1, min(cnt2, cnt3));
}
void solve()
{
cin >> a >> b >> c >> k;
if (k == a || k == b || k == c) //其实在dfs函数里预先判断这种情况就行了
{
cout << 0 << endl;
return;
}
if (k != 0 && k != 1 && k != 2)
{
cout << -1 << endl;
return;
}
cnt = dfs(a, b, c, 0);
if (cnt == 5) cout << -1 << endl;
cout << cnt << endl;
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> T;
while (T -- )
solve();
return 0;
}
范式 以后就按着这个模板思考
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T;
int a, b, c, k;
int res;
//不用去想那么多 所有数求出来的mex()最大就是2 肯定不会有很多重循环 直接循环就是了 不用在那特判
int mex(int a, int b){
for(int i = 0; ; i++){
if(i != a && i != b) return i;
}
}
//不设返回值 答案存在res里
void bfs(int a,int b,int c,int dep){
if (dep > 5) return;
if(a == k||b == k||c == k){ //把初始时k就等于a b c其中一个情况也融进去了 dep此时等于0
res = min(res, dep); return; //直接开一个res记录答案,避免复杂地设置变量
}
bfs(a,b,mex(a,b),dep+1);
bfs(a,mex(a,c),c,dep+1);
bfs(mex(b,c),b,c,dep+1);
}
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin>>T;
while(T--){
int a,b,c;
cin>>a>>b>>c>>k;
res=666; //初始时设成极大值,避免影响答案
bfs(a,b,c,0);
if(res==666)res=-1; //如果res没变,说明不可能操作后得到k
cout<<res<<endl;
}
}
E 立希喂猫 二分
Problem Statement
Taki买了 \(n\) 种猫粮,第 \(i\) 种猫粮的营养值为 \(a_i\) 、数量为 \(b_i\) 。
猫猫的饭量是无穷的,每一天她可以吃任意数量的猫粮,但是同一种猫粮她一天只会吃一次。
Taki想知道在 \(k\) 天内,猫猫可以获得的最大营养值之和是多少。
Input
第一行输入一个正整数 \(n(1 \leq n \leq 10^5)\) ,表示猫粮种数。
第二行输入 \(n\) 个正整数 \(a_i(1 \leq a_i \leq 10^4)\) ,表示每种猫粮的营养值。
第三行输入 \(n\) 个正整数 \(b_i(1 \leq b_i \leq 10^9)\) ,表示每种猫粮的数量。
第四行输入一个正整数 \(q(1 \leq q \leq 10^5)\) ,表示询问次数。
接下来 \(q\) 行,每行输入一个正整数 \(k(1 \leq k \leq 10^9)\) ,表示询问的天数。
Output
对于每个询问,在一行中输出一个整数表示答案。
Sample
输入
3
3 7 5
1 2 1
3
1
2
3
输出
15
22
22
说明
一种可行的方案是:
第 1 天:
猫猫吃第1、2、3种猫粮,营养值之和为3+7+5=15。
第 2 天:
猫猫吃第2种猫粮,营养值之和为7。
第 3 天:
猫猫不吃猫粮。
Solution
观察题目,关键点在同一种猫粮她一天只会吃一次,也就是说k天他最多吃这种猫粮k次,要是猫粮数量不够它最多吃 \(b_{i}\)次,这就是破局的关键,那么如果我们按照猫粮数量从小到大排序,必然前面的猫粮数量小于k,后面的猫粮数量大于k,这样就有二分的性质了,排好序的猫粮序列,前i种猫粮满足性质\(k > b_{i}\),后n-i种猫粮不满足此性质,这样就可以二分区间,找到满足性质的右边界,这种猫粮时最后一个满足\(k > b_{i}\)的猫粮,它在k天内能被吃完,营养值是\(a_{i}*b_{i}\),在这种猫粮后面的猫粮由于\(k < b_{i}\),k天内最多吃k次,营养值是\(k * a_{i}\)。
在这个过程中我们利用前缀和和后缀和优化,预先计算出前i个猫粮全能吃\(b_{i}\)次的营养值之和 和 后n-i个猫粮都吃不满\(b_{i}\)次 只能吃k次 的营养值之和 由于k后面才会给 我们就先算\(a_{i}\)之和 后面乘上k就行
这样预处理之后,二分算出右边界之后,我们就可以对算出的最后一个满足性质\(k > b_{i}\)的猫粮之前的猫粮利用前缀和 这之后的猫粮利用后缀和,算出总营养值,具体看代码
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, q, k;
int s1[N], s2[N];
struct Maoliang
{
int a, b;
bool operator < (Maoliang &W)const //重载运算符 按照每种猫粮的数量进行排序
{
return b < W.b;
}
}ml[N];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 0; i < n; i ++ )
{
int a;
cin >> a;
ml[i].a = a;
}
for (int i = 0; i < n; i ++ )
{
int b;
cin >> b;
ml[i].b = b;
}
//将猫粮按照数量排序
sort(ml, ml + n);
//求前i个猫粮的营养值*数量的前缀和
s1[0] = ml[0].a * ml[0].b;
for (int i = 1; i < n; i ++ ) s1[i] += s1[i - 1] + ml[i].a * ml[i].b;
//求第i到第n - 1个猫粮的营养值后缀和
s2[n - 1] = ml[n - 1].a;
for (int i = n - 2; i >= 0; i -- ) s2[i] += s2[i + 1] + ml[i].a;
cin >> q;
while (q -- )
{
cin >> k;
int l = 0, r = n - 1; //在第0~n-1种猫粮的区间内二分,找ml[i].b>=k的右边界
while (l < r)
{
int mid = (l + r + 1) / 2;
if (k >= ml[mid].b) l = mid;
else r = mid - 1;
}
if (r == 0 && k < ml[r].b) cout << k * s2[0] << endl; //特殊情况判断 当所有猫粮数量都比k大时,那我们所有的猫粮都只能吃k个 (被卡了好久)
else cout << s1[r] + s2[r + 1] * k << endl; //前r个猫粮可以吃ml[r].b个,后n - r个猫粮只能吃k个
}
//cout << ml[n - 1].a << " " << ml[n - 1].b << endl;
return 0;
}
刚开始模仿题目最大生产写出的题解 时间复杂度是n * q * logn 也就是\(O(n^2log_{2}{n})\),会超时
而且l的初始值设成0也调了好久
二分需要注意的两个点
- 待二分的区间必须是有序的
- 求左边界是
l + r >> 2
求右边界是l + r + 1 >> 2
一定不要忘了
暴力Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, q, k;
int a[N], b[N];
bool check(int mid)
{
//cout << k << endl;
int res = mid;
for (int i = 0; i < n; i ++ )
{
if (k <= b[i])
{
res -= k * a[i];
}
else
{
res -= b[i] * a[i];
}
if (res <= 0) return true;
}
return false;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 0; i < n; i ++ ) cin >> a[i];
for (int i = 0; i < n; i ++ ) cin >> b[i];
cin >> q;
while (q -- )
{
cin >> k;
int l = 0, r = 1e18; //注意!求右边界是int mid = (l + r + 1) / 2; 否则会出现陷入死循环问题 忘了导致debug半天
while (l < r)
{
cout << r << endl;
int mid = (l + r + 1) / 2;
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << r << endl;
}
return 0;
}
F 祥子拆团 数论
Problem Statement
Sakiko有两个数字 \(x,y\) ,她想知道,有多少种方式可以将 \(x\) 拆成 \(y\) 个正整数的乘积。
例如 \(x=6,y=2\) 时,有 \(6 \times 1=6,3 \times 2=6,2 \times 3=6,1 \times 6=6\) 这 4 种方法。
由于这个答案可能很大,因此你需要输出答案对 \(10^9 + 7\) 取模后的结果。
Input
第一行输入一个正整数 \(T(1 \leq T \leq 10^3)\) ,表示询问次数。
接下来 \(T\) 行,每行输入两个正整数 \(x,y(1 \leq x,y \leq 10^9)\) ,表示询问。
Output
对于每个询问,在一行中输出一个整数表示答案。由于这个答案可能很大,因此你需要输出答案对 \(10^9 + 7\) 取模后的结果。
Sample
输入
2
6 2
12 2
输出
4
6
说明
第 2 个询问:
[12,1],[6,2],[4,3],[3,4],[2,6],[1,12]
答案为6
Solution
这个题我是这么理解的,我们先用i找x的质因子(i < x / i , 只用找一对因子中的一个),找到一个因子i,用while (x / i == 0) x /= i; d++;
把x除尽,这时候cnt就是x里质因子i的个数(幂),这时候由于x内不同的质因子彼此独立(前提条件),while循环结束后我们将质因子i全部从x里分离了出来,个数为d,下一次for循环再去筛下一个质因子,相当于到最后我们把x变成了由其多个质因子的d次方构成的数
找到一个因子i后,我们将其不断从x内分离,最后一次x /= i
完成后得到不能被i取余的质因子x,相当于把x拆成了\(i^d*x\),一共d+1个因子,要满足拆成y个正整数乘积,其余y-d-1个空位补上1,我们就能满足题目要求的 拆成 \(y\) 个正整数的乘积。
由隔板法,将d个球放进y个盒子内,盒子可以有空位,可以看成有n个相同的球,m个不同的盒子,把n个球放到盒子里,盒子允许为空,有多少种方案,把d个因子看成n个相同的球,把y个数看成m个不同的盒子,把d个球放到盒子里,盒子允许为空,有多少种方案。 d个球有d-1个空隙,这里面可以利用隔板插进空隙将球分成y组 有C(y-1,d)种方案,由于盒子可以为空,则为 C(y + d - 1, d),这是对于x的一个因子i的情况,由于x有很多因子,每一个因子都有方法数C(y + d - 1, d),则彼此之间累乘即可
在计算组合数时,因为最终结果都会模上MOD,所以可以将除法转化为快速幂求逆元
\(C^{m}_{n} = \frac{n!}{m!(n - m)!} = \frac{n(n - 1)(n - 2)...(n - m + 1)}{m!}\) 本题$n = y + d -1, m= d $
我们就可以将这里分子的除以m!转化为对m m - 1 ...1 求逆元再相乘 这就是快速幂求逆元的运用
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T;
int x, y;
const int mod = 1e9 + 7; //这里是1e9+7 刚开始写成10e9 debug半天
//快速幂求逆元
int qmi(int a, int b)
{
int res = 1 % mod;
while (b)
{
if (b & 1)
res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
//cout << res << endl;
return res;
}
void solve()
{
cin >> x >> y;
int ans = 1;
//找出x的所有质因子 先筛因数的幂
for (int i = 2; i * i <= x; i ++ )
{
int d = 0;
if (x % i != 0) continue;
while (x % i == 0)
{
x /= i;
d ++ ;
}
//根据我们推导出的组合数公式求出每一个因子i的组合数
int tmp = 1;
for (int j = y; j <= d + y - 1; j ++ ) //分子的第一项n-m+1 就是y+d-1 - d + 1 = y 最后一项n就是y+d-1
{
tmp = tmp * j % mod; //先算分子
tmp = tmp * qmi(j - y + 1, mod - 2) % mod; //除以分母 j - y + 1 也就是对应的1 2 ... m-1 m (m!)
//cout << qmi(j - y + 1, mod - 2) << endl;
}
//cout << tmp << endl;
ans = ans * tmp % mod; //最终的答案是每一个因子i的方案数相乘
}
//如果上面对x所有的因子筛完之后 x是一个不等于1的质数 他也是x的一个质因子 他的幂d=1 还要组合一次
if (x > 1)
{
int d = 1, tmp = 1;
for (int j = y; j <= d + y - 1; j ++ ) //分子的第一项n-m+1 就是y+d-1 - d + 1 = y 最后一项n就是y+d-1
{
tmp = tmp * j % mod; //先算分子
tmp = tmp * qmi(j - y + 1, mod - 2) % mod; //除以分母 j - y + 1 也就是对应的1 2 ... m-1 m (m!)
}
ans = ans * tmp % mod;
//cout << tmp << endl;
}
cout << ans << endl;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> T;
while (T -- )
{
solve();
}
return 0;
}