Educational Codeforces Round 50(div. 2) 题面 & 题解

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} \rceil\)

代码

#include <iostream>
using namespace std;
long long n, k;//不用 double 和 ceil(),防止精度爆炸。
int main(){
	cin >> n >> k;
	cout << (k+n-1) / n << '\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) \rightarrow\) \((x+1,y+1) \rightarrow (x+2,y)\) 更优的达到。所以说我们首先可以通过连续的斜步先走到 \((\min(x,y),\min(x,y))\), 然后剩下的要走的路就是一条线段。显然可以发现,走到目的地的最少步数为 \(\max(x,y)\)\(\min(x,y)\) 步走完第一部分,\(\max(x,y)-\min(x,y)\) 走完剩下的线段)。于是无解的情况就被我们讨论完了。于是我们接着分类讨论:

  • 如果此离目的地还有偶数个直步(已经判断完了无解,下同):

    1. 如果剩偶数步可以走,那么就在最后的步数中一直反复走“斜步”。最终答案为 \(k\)

    2. 如果剩奇数步可以走, 那么把一开始走到 \((\min(x,y),\min(x,y))\) 的连续斜步中的一个斜步改成两个直步,于是就变成了 上面 1 的情况了。最终答案为 \(k-2\)

  • 如果距离目的地还剩奇数个直步:

    1. 如果剩偶数步可以走,则先用类似 1 的方法走到一个离终点距离为 \(1\) 个直步,且离当前位置最近的点,再一个斜步走到目的地另一个离终点距离为 \(1\) 个直步的点,再一个直步走到目的地。此时变成了 1 的情况。最终答案为 \(k-1\)

    2. 如果剩奇数步可以走,则一个直步向着目的地走,则变成了 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 << '\n';
		return;
	}
	k -= mx;
	if((mx-mn)%2 == 0){
		if(k % 2 == 1) cout << k+mx-2 << '\n';
		else cout << k+mx << '\n';
	}else{
		cout << k+mx-1 << '\n';
	}
}

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) << '\n';
}

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 \times 10^5; \;a_i,b_i \in[1,10^9]\)

题解

很明显无解当且仅当 \(\sum a \neq \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 << '\n';
		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 << '\n';
	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, b\in \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 << '\n';
}

signed main(){
	get_mu(65);
	int T; read(T);
	while(T--) Main();
	return 0;
}
posted @ 2020-12-27 22:05  zimindaada  阅读(177)  评论(0编辑  收藏  举报