cf1632 Codeforces Round #769 (Div. 2)题解

A ABC

水题0.0

B Roof Construction

构造题,经过实验构造出了0的右边是不超过\(n\)的最大的\(2^k\rightarrow n\),左边是\(2^k-1,2^k-2,..,1\)

明天起来看看B的证明。

可恶!出题人不写证明,那我也摸了

C Strange Test

给定两个整数\(a,b~(a<b)\),可以执行下列三种操作,每种操作都可以进行任意次:

  • \(a:=a+1\)
  • \(b:=b+1\)
  • \(a:=a|b\)

要求输出最少需要多少次操作,可以使得\(a,b\)相等?

数据范围:\(1\leq t \leq 10^4;1\leq a<b\leq 10^6;\sum b\leq 10^6\)

猜的结论,是要么\(a:=a+1\),然后进行按位或操作;要么一直\(b:=b+1\),然后最后只会进行最多一次按位或操作。所以求出来这两种方法得到的答案,然后比较一下即可。

int x, y;
const int INF = 0x3f3f3f3f;

void solve(){
	scanf("%d%d", &x, &y);
	int r1 = 0, r2 = 0, res = y - x;
	int a = x, b = y;
	while ((a|b) != b && r1 <= res){
		++a, ++r1;
	}
	if (a != b) ++r1;
	a = x, b = y;
	while ((a|b) != b && r2 <= res){
		++b, ++r2;
	}
	if (a != b) ++r2;
	res = min({res, r1, r2});
	printf("%d\n", res);
}

正解当然不应该这么做!具体怎么做的明天再看。

首先,第三种操作最多用一次。因为,它不会减少\(a\),并且在操作后一定有\(b\leq a\)。这意味着,在进行操作3后,我们最多只能进行若干次操作2了。

  • 如果我们不使用第三种操作,那么答案为\(b-a\)

  • 如果我们先使用了若干次操作1和操作2,然后使用了一次操作3,最后又使用了若干次操作2。

    假设经过这些操作后,\(a\rightarrow a',b\rightarrow b'(a\leq a',b\leq b')\)

    此时花费的操作数为\((a'-a)+(b'-b)+1+((a'|b')-b')=a'+(a'|b')+(1-a-b)\)

为了找到最优的答案,我们可以迭代\(a'\),令\(a'=a:b\),对于固定的\(a'\),我们要做的就是最小化\(a'|b'\),设\(b'\)初始值为0,然后从最高位到最低位迭代,有如下四种情况:

  • 当前位\(a'=0,b=1\),那么当前位\(b'\)填1;
  • 当前位\(a'=0,b=0\),那么当前位\(b'\)填0;
  • 当前位\(a'=1,b=1\),那么当前位\(b'\)填1;
  • 当前位\(a'=1,b=0\),那么当前位\(b'\)填1,终止迭代。

时间复杂度\(O(b\log b)\)或者\(O(b)\)。(第二种情况直接使用位运算把\(O(\log b)\rightarrow O(1)\)。)

嗯,这样就是最优的没有问题!

题目的Bonus:证明最优情况一定为\(a'=a\),或者\(b'=b\)。(也就是猜的结论)

int a, b;

void solve(){
	scanf("%d%d", &a, &b);
	int mn = b - a;
	for (int ax=a;ax<=b;++ax){
		int bx = 0, cur;
		for (int i=20;i>=0;--i){
			if (b >> i & 1){
				bx += (1 << i);
			}
			else if (ax >> i & 1){
				bx += (1 << i);
				break;
			}
		}
		cur = ax + (ax | bx) - a - b + 1;
		mn = min(mn, cur);
	}
	printf("%d\n", mn);
}

D New Year Concert

有一个数组\(a[n]\),令\(a[n]\)的长度为\(k\)的前缀子序列\(b[k]=\{a_1,a_2,..,a_{k}\}\),如果其中有任何一个连续区间满足\(1\leq l\leq r\leq k,\gcd(b_l,b_{l+1},..,b_{r})=r-l+1\),那么就称这个序列是boring的。

为了避免\(b[k]\)boring,可以进行以下操作若干次,将\(b[k]\)中的一个元素\(b_t\)\(d\)替换,即\(b_t=d\)。注意,每次操作的\(d,t\)都可以是不同的。定义\(f(b)\)为令数组\(b\)不再boring的最小操作次数,对于所有\(a[n]\)的前缀,求\(f([a_1]),f([a_1,a_2]),..,f([a_1,a_2,..,a_n])\)

数据范围:\(1\leq n \leq 2\times 10^5;1\leq a_i \leq 10^9\)

嗯……首先,对于固定的左端点\(l\),随着\(r\)的增大,\(gcd(a_l,..,a_r)\)一定是不递增的。那么就有了一个很重要的性质:固定左端点,满足\(gcd(a_l,..,a_r)=r+l-1\)\(r\)仅有一个,假如满足条件的为\(r'\)。从这里也可以看出来,boring子序列的数量最多不超过\(n\)个。那么怎样就可以消除这个boring呢?答案是在\((a_l,a_{r'})\)中的任一个数中间,修改\(a_k=\)某一个很大的质数,这个大质数要满足后面没有数是它的倍数,这里不妨设这个大质数\(p=10^9+7\)

如果我们令\(a_i=p\),那么所有包含\(i\)的区间,boring都会消失。这是一个标准的贪心问题:找到最小的\(r\),然后所有包含\(r\)的段不再boring,删除这些段,然后继续操作。

嗯……我看的题解思路差不多,就是固定\(r\),然后找\(l\),定义\(pos\)为上一个修改了\(a[pos]\)的右端点,然后循环到\(d\geq i-pos\)时停止,当\(d=i-pos\)时,那就修改当前的\(i\),则\(pos=i\)。当然,也可以使用二分找到这样的\(d\)。区间查询\(gcd\),使用线段树即可。

(题解写的代码什么玩意 看不懂QAQ)

int n;
const int N = 2e5 + 5;
int a[N];
struct node{
	int le, ri, v;
};
node seg[N<<2];

int gcd(int a, int b){
	return b ? gcd(b, a % b) : a;
}

void pushup(int oo){
	seg[oo].v = gcd(seg[ls].v, seg[rs].v);
}

void build(int oo, int le, int ri){
	seg[oo] = {le, ri, 0};
	if (le >= ri){
		seg[oo].v = a[le];
		return ;
	}
	int mid = (le + ri) >> 1;
	build(ls, le, mid);
	build(rs, mid + 1, ri);
	pushup(oo);
}

int query(int oo, int x, int y){
	int le = seg[oo].le, ri = seg[oo].ri;
	if (le >= x && ri <= y){
		return seg[oo].v;
	}
	int res = 0, mid = (le + ri) >> 1;
	if (x <= mid){
		res = gcd(res, query(ls, x, y));
	}
	if (y > mid){
		res = gcd(res, query(rs, x, y));
	}
	return res;
}

int main(void){
	scanf("%d", &n);
	for (int i=1;i<=n;++i){
		scanf("%d", &a[i]);
	}
	build(1, 1, n);
	int res = 0, pos = 0;
	for (int i=1;i<=n;++i){
		int d = query(1, pos + 1, i);
		while (d < i - pos){
			pos++;
			d = query(1, pos + 1, i);
		}
		if (d == i - pos){
			res++, pos = i;
		}
		printf(i==n?"%d\n":"%d ", res);
	}
	return 0;
}

E1&E2 Distance Tree(easy & hard version )

有一棵树,根节点为1,每条边的权值为1。定义\(d(v)\)为从结点1到结点\(v\)的距离。假设从结点\(a\)出发,向某个结点连\(b\)了一条长度为\(x\)的边,定义\(f(x)=\max\{d(v)|1\leq v\leq n\}\),要求输出\(f(x)\)的最小值,\(1\leq x\leq n\)

数据范围:\(1\leq n \leq 3\times 10^5\)

首先,应该是从结点1向其他的某个结点连边。然后要求最长的路径最短,可以想到是二分。如果我们连边的话,假设从结点1向结点\(b\)连了一个长度为\(x\)的边,并假设此时所有的边都\(\leq mid\)。那么,所有的结点要么直接按照原来的树走到1,路径长度\(\leq mid\),要么就是通过新连的边到达1,新的路径长度\(\leq mid\)。那么对于\(mid\),我们只要检查所有的\(>mid\)的路径能否变成\(\leq mid\)的路径。

\(rt\)为到结点1距离最远的结点,则\(f[i]\):和结点1的距离\(\geq i\)时,到结点\(rt\)的最大距离。显然,我们会找到一个原本路径\(\geq i\)的点集中,两点间的最长距离。连接这个最长路的中点\(u\)和结点1,则剩下的所有点的距离都会\(\leq x+\lceil \frac{dis[u][1]}{2} \rceil\)。而当\(x\)过大时,这条路径会变得还不如原本路径,因此要和\(dis[rt][1]\)取一个最小值。

int n, rt;
const int N = 3e5 + 5;
vector<int> ve[N];
int d[N][2], f[N]; 
// d[i][0]:到结点1的距离; d[i][1]:到结点rt的距离 
// f[i]: 和结点1的距离为>=i时,  到结点rt的最大距离 

void init(){
	for (int i=0;i<=n;++i){
		ve[i].clear();
		f[i] = d[i][0] = d[i][1] = 0;
	}
}

void dfs(int u, int pre, int x){
	if (pre) d[u][x] = d[pre][x] + 1;
	for (auto to : ve[u]){
		if (to == pre) continue;
		dfs(to, u, x); 
	}
}

bool check(int x, int v){
	return x >= min(d[rt][0], v + (f[x + 1] + 1) / 2);
}
 
void solve(){
	n = read();
	init();
	int u, v;
	for (int i=1;i<n;++i){
		u = read(), v = read();
		ve[u].push_back(v), ve[v].push_back(u);
	}
	dfs(1, 0, 0);
	int mx = -1;
	for (int i=1;i<=n;++i){
		if (mx < d[i][0]){
			mx = d[i][0], rt = i;
		}
	}
	dfs(rt, 0, 1);
	for (int i=1;i<=n;++i){
		f[d[i][0]] = max(f[d[i][0]], d[i][1]);
	}
	for (int i=n-1;i>=0;--i){
		f[i] = max(f[i], f[i + 1]); 
	}
	
	for (int i=1;i<=n;++i){
		int L = 0, R = n, ans = n;
		while (L <= R){
			int mid = (L + R) >> 1;
			if (check(mid, i)){
				ans = mid;
				R = mid - 1;
			}
			else L = mid + 1;
		}
		printf("%d ", ans);
	}
	puts("");
}
posted @ 2022-09-08 20:08  跳岩  阅读(38)  评论(0编辑  收藏  举报