Educational Codeforces Round 50(div. 2)题面 & 题解
被教练抓去写这个东西了……
题号 | 题目名 | 难度系数 | 算法标签 |
---|---|---|---|
A | Function Height | 15 | 贪心,基础数学 |
B | Diagonal Walking v.2 | 25 | 贪心,找规律 |
C | Classy Numbers | 40 | 基本数位DP |
D | Vasya and Arrays | 35 | 贪心,二分查找 |
E | Covered Points | 45 | 初中计算几何,模拟,暴力 |
F | Relatively Prime Powers | 50 | 数论,反向思维,卡精度 |
A Function Height
题意
给定平面直角坐标系 (OXY) ,其中 (x) 轴上有 (2n+1) 个点 (P_0, P_1, dots, P_{2n}) ,初始 (P_i) 坐标为 ((i,0)) 。每次操作,你可以对 (i) 为奇数的点的 (y) 轴坐标 (+1) 。现在你要对这些点做若干次操作,使得线段 (P_iP_{i+1}) 和 x 轴所围成的图形面积恰好为 (k) ,并且使所有点的 (y) 坐标的最大值最小。求这个最小值。 (n, k leq 10^{18})
题解
由题,易知一共有 (n) 个可被操作的点,且每次操作会且只会使总面积 (+1) ,所以我们操作最多为 (k) 次。所以我们的移动应尽可能平均(即让每个可被操作的点高度尽可能接近)。容易知道此时答案一定最小,且答案为 (lceil frac{k}{n} ceil) 。
代码
#include <iostream>
using namespace std;
long long n, k;//不用 double 和 ceil(),防止精度爆炸。
int main(){
cin >> n >> k;
cout << (k+n-1) / n << '
';
return 0;
}
B Diagonal Walking v.2
题意
你在一个平面直角坐标系上走,初始你在原点。你每次可以从 ((x,y)) 走到:((x+1,y)), ((x-1,y)), ((x,y+1)), ((x,y-1)), ((x+1,y+1)), ((x+1,y-1)), ((x-1,y+1)),((x-1,y-1)) 八个点。如果在一步中,你选择走到上面的后四种选择之一,则这一步被称之为“斜步”,而其他的步被称为“直步”。你可以经过一个点任意多次。
有 (q (q leq 10^4)) 个询问,每一次询问是否恰好能走 (k) 步走到 ((x,y)) ,如果能,求走路过程中最多能走几个“斜步”;如果不能,输出-1
。 (k,x,y leq 10^{18}) 。
题解
假设 (x,y > 0) 。显然所有的 (x,y) 的情况都有等价的 (x,y > 0) 的情况(即 (x = |x|, y = |y|) )。
易知:对于向上下左右四个方向走 (2) 步的情况都可以通过走两次“斜步”来代替。例如,从 ((x,y)) 走到 ((x+2,y)) 可以由 ((x,y) ightarrow) ((x+1,y+1) ightarrow (x+2,y)) 更优的达到。所以说我们首先可以通过连续的斜步先走到 ((min(x,y),min(x,y))), 然后剩下的要走的路就是一条线段。显然可以发现,走到目的地的最少步数为 (max(x,y)) ((min(x,y)) 步走完第一部分,(max(x,y)-min(x,y)) 走完剩下的线段)。于是无解的情况就被我们讨论完了。于是我们接着分类讨论:
-
如果此离目的地还有偶数个直步(已经判断完了无解,下同):
-
如果剩偶数步可以走,那么就在最后的步数中一直反复走“斜步”。最终答案为 (k) 。
-
如果剩奇数步可以走, 那么把一开始走到 ((min(x,y),min(x,y))) 的连续斜步中的一个斜步改成两个直步,于是就变成了 上面 1 的情况了。最终答案为 (k-2) 。
-
-
如果距离目的地还剩奇数个直步:
-
如果剩偶数步可以走,则先用类似 1 的方法走到一个离终点距离为 (1) 个直步,且离当前位置最近的点,再一个斜步走到目的地另一个离终点距离为 (1) 个直步的点,再一个直步走到目的地。此时变成了 1 的情况。最终答案为 (k-1) 。
-
如果剩奇数步可以走,则一个直步向着目的地走,则变成了 1 的情况了。最终答案为 (k-1) 。
-
(看不懂的话可以自己画图理解。)
一次询问时间复杂度 (O(1)), 总时间复杂度 (O(q)) 。
代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
template<typename T> inline T read(T& t) {//快读,这道题可以用scanf读入
t=0;short f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-f;ch=getchar();}
while (ch>='0'&&ch<='9') t=t*10+ch-'0',ch=getchar();
t*=f; return t;
}
ll n, m, k, mn, mx;
inline void Main(){
read(n); read(m); read(k);
if(n < 0) n = -n; //把(n,m)改成正的
if(m < 0) m = -m;
mn = min(n,m), mx = max(n,m);
if(mx > k) {//步数少于最少需要的步数,这是唯一无解的情况。
cout << -1 << '
';
return;
}
k -= mx;
if((mx-mn)%2 == 0){
if(k % 2 == 1) cout << k+mx-2 << '
';
else cout << k+mx << '
';
}else{
cout << k+mx-1 << '
';
}
}
signed main(){
int q; read(q);
while(q--) Main();
return 0;
}
C Classy Numbers
题面
定义一个数字是“好数”,当且仅当它的十进制表示下有不超过 (3) 个数字(1 sim 9)。举个例子:(4,200000,10203) 是“好数”,然而 (4231,102306,7277420000) 不是。
(T(1 le T le 10^{4})) 组数据。每组数据给你 (l,r ; (1 le l le r le 10^{18})),问有多少个 (x) ((l le x le r)) ,且 (x) 是“好数”。
题解
模板数位 DP 题。
我们令 (dp_{i,j}) 表示在考虑第 (i) 位,并且非 (0) 的数码有 (j) 个,且第 (i) 位没有选择限制的 后面比他小的位的方案数。我们用记忆化搜索的方式来填 (dp_{i,j}) 。首先我们把原题的求区间改成两个前缀和相减,然后我们分别 (r) 和 (l) 计算前缀和答案。
在求到 (x) 前缀和答案的过程中,我们首先先把 (x) 拆成他的每一个位,然后从最高位开始进行 dfs
。我们发现,如果在某一位选的比 (x) 的那一位要小,那么后面的每一个位不管怎么选,都不会比 (x) 要大,那么这些的答案就将会是通用的,可以把他们记忆化。具体的一些细节在代码里面讲。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
namespace ztd{
using namespace std;
typedef long long ll;
template<typename T> inline T read(T& t) {//fast read
t=0;short f=1;char ch=getchar();double d = 0.1;
while (ch<'0'||ch>'9') {if (ch=='-') f=-f;ch=getchar();}
while (ch>='0'&&ch<='9') t=t*10+ch-'0',ch=getchar();
if(ch=='.'){ch=getchar();while(ch<='9'&&ch>='0') t+=d*(ch^48),d*=0.1,ch=getchar();}
t*=f;
return t;
}
}
using namespace ztd;
ll dp[20][5];
ll l, r;
ll a[20];
ll dfs(int x, int num, bool flag){//分别代表当前搜到的位数、有多少非0数码、是否还在贴着上界进行dfs(贴着就不能进行记忆化)
if(!x) return 1;//如果搜完了,搜到了底端,说明当前这个数可以
if(!flag && dp[x][num] != -1) return dp[x][num];//搜过了,且现在不贴着上界,就可以用dp记忆化过的东西了。
int uplim = flag ? a[x] : 9;//如果现在贴着上界,那么这一位只能不大于上界的这一位,否则随便。
ll ret = 0;
for(int i = 0; i <= uplim; ++i){
if(i == 0) ret += dfs(x-1, num, (flag && i == a[x]));//如果是0就不计入非0数码,
else if(num < 3) ret += dfs(x-1, num+1, (flag && i == a[x]));//是的话就计入。
}
if(!flag) dp[x][num] = ret;//如果当前不贴着上界搜索,当前状态就可以复用,就可以记忆化
return ret;
}
ll work(ll x){
ll t = 0;
while(x){//拆位
a[++t] = x % 10;
x /= 10;
}
return dfs(t, 0, 1);
}
inline void Main(){
read(l); read(r);
cout << work(r) - work(l-1) << '
';
}
signed main(){
memset(dp,-1,sizeof(dp));//由于这道题的状态可以复用,所以在开始初始化一次即可。
int T; read(T);
while(T--) Main();
return 0;
}
D Vasya and Arrays
题面
给你两个数组(a, b),要求把这两个数组各分成 (k) 段(段必须是下标连续的元素),使得 (a,b) 从左到右对应的每一段,里面的元素和相等。求 (k) 的最大值。如果无解,则输出 -1
。数组长度 (n, m leq 3 imes 10^5; ;a_i,b_i in[1,10^9]) 。
题解
很明显无解当且仅当 (sum a eq sum b) (否则肯定可以把 (a,b) 全部变成一段)。
可以发现有一个贪心:在分每一个段的时候,找 (a) 和 (b) 的两个前缀,使得这两个前缀的和相等,且这两个前缀尽可能的短,一定是最优解。这是非常显然的,读者自证不难。
然后我们发现最优解的一个显然的性质:在每一个对应的段的分段点,他们的前缀和相等,且反之亦然。那末我们就可以遍历 (a),对于(a) 里面的每一个元素的前缀和,lower_bound
查找一下 (b) 中有没有前缀和相等的点,有的话,(a,b) 这两个地方就是分段点,没有的话就不是。显然时间复杂度是 (O(n log n)) 的。
代码
#include <iostream>
#include <cstdio>
namespace ztd{
using namespace std;
typedef long long ll;
template<typename T> inline T read(T& t) {//fast read
t=0;short f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-f;ch=getchar();}
while (ch>='0'&&ch<='9') t=t*10+ch-'0',ch=getchar();
t*=f; return t;
}
}
using namespace ztd;
const int maxn = 3e5+7;
int n, m, a[maxn], b[maxn];
ll sa[maxn], sb[maxn];
signed main(){
read(n);
for(int i = 1; i <= n; ++i){
read(a[i]);
sa[i] = sa[i-1] + a[i];
}
read(m);
for(int i = 1; i <= m; ++i){
read(b[i]);
sb[i] = sb[i-1] + b[i];
}
if(sa[n] != sb[m]) {
cout << -1 << '
';
return 0;
}
int ans = 0;
for(int i = 1; i <= n; ++i){
int tmp = lower_bound(sb+1, sb+m+1, sa[i]) - sb;
if(sb[tmp] == sa[i]) ++ans;
}
cout << ans << '
';
return 0;
}
E Covered Points
题面
有(n;(1 le n le 1000))条线段,问它们覆盖了多少个整点(即 ((x,y)) 都为整数的点)。点的坐标范围是([-10^6,10^6])整数,保证每条线段不会退化成点,保证没有两条线段在同一直线上。
题解
暴力。每次加进去一条线段,就算出他所经过的整点的数量(可以通过 (gcd(|A_x-B_x|, |A_y-B_y|)+1) 来计算),然后暴力遍历已经加进去的线,通过形如 (k_1x+b_1=k_2x+b_2) 的一次方程式求出交点的坐标(小技巧:可以把式子化成 ((k_1-k_2)x = b_2-b_1) 后,可以直接通过取模判断 (b_2-b_1) 是否整除于 (k_1-k_2) , 防止精度爆炸)并保证交点确实在线段上而不是只在直线上。时间复杂度 (O(n^2)) ,注意精度。
代码
咕了
F Relatively Prime Powers
题面
对于一个数 (x),当且仅当将其分解后各个质因数的指数的最大公约数为 (1) 时,我们称这个数是合法的。
比如:例子:(5(=5^1,gcd(1)=1))、(12(=2^2*3^1,gcd(2,1)=1))、(72(=2^3*3^2,gcd(3,2)=1)) 是合法的,
而 (8(=2^3,gcd(3)=3))、(100(=2^2*5^2,gcd(2,2))) 不是合法的。
有 (T) 组数据,对于每组数据,询问区间 ([2,n]) 中合法的数的个数。
题解
我们容易发现,指数不会超过 (60),因为 (2^{60} > 10^{18})。
很容易知道,所有数都可以表示成 (a ^ b),其中 (a, bin mathbb N^+) ,且 $b = $ 因数个数的 (gcd) 。由前面发现指数上限很小,所以考虑从指数方面入手。我们要求的就是 (b) 最大只能表示成 (1) 的,而这明显难求。所以我们考虑容斥,即求 (b=1, b=2, b=3, dots) 的情况的答案 (ans_b),然后再用容斥系数来容斥,即变成 (ans = p_1ans_1 + p_2ans_2 + dots) 类似的形式。
从头入手:(b=1) 很明显容斥系数为 (1) (不为 (1) 的话,后面质数什么的你“斥”不掉),(b = 2, b= 3) 容斥系数为 (-1)。(b = 4) 的答案由于是 (b=2) 的真子集,所以容斥系数为 (0) 。(b=5) 时为 (-1), (b = 6) 的时候,由于它的答案恰好是 (b=2,b=3) 的交集,所以容斥系数为 (1)。细细推下去,发现容斥系数是什么?是莫比乌斯函数 (mu) !那么就简单了。考虑每一个指数 (b),考虑它的底数 (a) 最大为多少,排除掉 (1), 然后就乘上一个 (mu) 就完事了。
那么怎么对于一个 (b) 求 (a) 最大值呢?
- 我会暴力!
很明显,TLE。
- 我会
pow
加上基本幂运算规则!(a^{frac{1}{x}}) = (sqrt[x]{a}) !
但是由于pow
的精度感人,你只能达到在第二个点 WA然后无能狂怒半个上午了的好成绩了。
那么怎么办呢?我们发现问题在于pow
的误差是不一定偏大/偏小的,那么我们只要保证他求出来的底数最大值 (a) 永远偏大一些,再用一些常数从误差值开始,用快速幂看有没有超限,来向下找到准确值。这样子就可以杜绝误差了。
记得快速幂里面用long double
,否则你就会和我一样由于爆ll
再无能狂怒一个中午了
时间复杂度 (O(60T))。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
namespace ztd{
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double db; //一定要用long double! 会爆long long! long double
template<typename T> inline T read(T& t) {//fast read
t=0;short f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-f;ch=getchar();}
while (ch>='0'&&ch<='9') t=t*10+ch-'0',ch=getchar();
t*=f; return t;
}
inline db ksm(db x, ll y){
db ret = 1;
while(y){if(y & 1) ret = ret * x; y >>= 1; x = x*x;}
return ret;
}
}
using namespace ztd;
int mu[70], prime[70], pcnt;
bool vis[70];
inline void get_mu(int uplimit){//预处理莫比乌斯函数
mu[1] = 1;
for(register int i = 2; i <= uplimit; ++i){
if(!vis[i]) prime[++pcnt] = i, mu[i] = -1;
for(register int j = 1; j <= pcnt && i*prime[j] <= uplimit; ++j){
vis[i*prime[j]] = 1;
if(i % prime[j] == 0) break;
mu[i*prime[j]] = -mu[i];
}
}
}
ll n;
inline void Main(){
read(n);
ll ans = 0;
for(register int i = 1; i <= 60; ++i){
if((1ll << i) <= n){
if(i >= 38){//3^38 > 1e18,而如果能进到循环里面说明2^i <= n,所以这可以卡一些精度
ans += mu[i];
continue;
}
//tips : 1.0L 表示 long double
ll uplimit = (ll)(pow(n, 1.0L/i)+2);//找到一个近似(必定不比实际小)的上限
while(ksm(uplimit,i) > n) --uplimit;//找到实际的上限
ans += mu[i] * (uplimit-1);//要排除掉1!
}else break;
}
cout << ans << '
';
}
signed main(){
get_mu(65);
int T; read(T);
while(T--) Main();
return 0;
}