周总结2022.1.17-2022.1.23

1.17

Luogu

P3384 【模板】轻重链剖分/树链剖分

学习了树链剖分(Lint-cut Tree)!

虽然之前也有看过,但是今天是第一次自己动手写。学完之后,感觉是一个非常巧妙的东西。树链剖分给我的感觉是,通过dfs序将一些修改操作挪到线段树、或者其他的数据结构上去。这种轻重链的划分,有助于快速跳点,如用树链剖分求LCA的时候,正常的求LCA做法是倍增,用树链剖分求的时候,直接跳到轻重链的顶端的操作取代了倍增的操作。但是树链剖分不稳定,很容易被卡,然后就退化成了一个一个跳,时间复杂度就不优了。

树链剖分的一般步骤+思想:

一、轻重结点和轻重链

在一棵树上,一个结点的诸多儿子当中,子树最大的儿子是重儿子,其他的都是轻儿子。

父结点与重儿子所连的边为重边,其他的边是轻边

多个相邻的重边所构成的链为重链,多个相邻的轻边构成的链为轻链

二、已知树根,确定一棵树的基本信息

对于树上的每一个结点,我们要统计的信息有:

fa[x]:x结点的父结点

son[x]:x结点的重儿子

dep[x]:x结点的深度

top[x]:x结点所在重链的顶端结点

id[x]:重编号后x结点的编号

size[x]:x结点的子树大小(包括x结点自身)

三、统计信息(两次dfs)

第一次dfs,统计树上每一个结点的父结点、重儿子、子结点的子树大小和结点的深度。我们通过dfs的方式从根结点不断深入,子结点继承父结点编号及其深度信息,父结点通过子结点返回的子树大小信息来更新重儿子信息和子树大小信息。

void dfs1(LL u, LL Fa) {
	size[u] = 1;
	for (LL i = front[u]; i; i = e[i].next) {
		LL v = e[i].v;
		if (v == Fa) continue;
		dep[v] = dep[u] + 1;
		fa[v] = u;
		dfs1(v, u);
		size[u] += size[v];
		if (size[v] > size[son[u]]) son[u] = v;
	}
}

第二次dfs,通过第一次dfs统计的信息,来构建重链以及重编号。我们将父结点与其重儿子之间连接上一条重边,这一步操作不需要我们对那一条边打上标记,而是通过传承重链顶点的方式来获取这条边是否是重链的信息。如果这个儿子不是重儿子,那么他作为父结点往下引申重链的时候,他自己会作为重链的顶点。

所以我们dfs时,对于每一个结点,遍历其子结点,如果这个子结点是它的重儿子,那么传递当前重链的顶点,向这个儿子继续dfs;如果这个子结点是它的轻儿子,那么令这个轻儿子为自己此时所处重链的顶点, 向这个儿子继续dfs。

在进行编号的过程中,我们使用dfs序对其进行重编号。而此时,我们优先访问重儿子,因为重儿子的子树结点数最多,选到这棵子树的概率就很大,期望上可以缩短时间。所以我们先把重儿子的dfs掉,然后再遍历剩下的轻儿子,剩下的轻儿子的顺序就没有那么大必要按照顺序来寻找了,因为轻儿子所连的子树的点和当前结点之间的编号已经断开了,就算想要放到线段树上去统计,也要分段来统计了,所以就没有按照子树大小排序的必要了。

void dfs2(LL u, LL t) {
	id[u] = ++cnt;
	a[id[u]] = w[u];
	top[u] = t;
	if (son[u]) {
		dfs2(son[u], t);
	}
	for (LL i = front[u]; i; i = e[i].next) {
		LL v = e[i].v;
		if (v != fa[u] && v != son[u]) {
			dfs2(v, v);
		}
	}
}

四、利用树链剖分求LCA

首先看这两个点是否在一条重链上,如果在,则直接选取深度最小的点作为LCA。

如果不在,则将深度大的往上跳,跳到其所在的重链的顶端的父结点。

只是跳到顶端的话,并没有脱离这条重链,要调到这个重链之外的话,必须要跳到这个顶端的父结点上去。

直到两个点都跳到在一条重链上为止,此时两个点的LCA是深度较小的那个点。

LL lca(LL x, LL y) {
	LL fx = top[x], fy = top[y];
	while (fx != fy) {
		if (dep[fx] < dep[fy]) {
			swap(fx, fy); swap(x, y);
		}
		x = fa[fx], fx = top[x];
	}
	return dep[x] < dep[y] ? x : y;
}

五、利用重编号的连续性构建线段树,对两点路径上的点进行修改/查询

和求LCA的思路大同小异。因为本质上,如果我们要求两个点之间的路径上的点,本质上,路径上的深度拐点就是这两个点的LCA。我们在让点往上跳的时候,顺带在线段树上求一下跳上去的这一段的信息就好(此时跳过的是重链,根据我们在dfs上的建法,这里的编号一定是连续的)。

最后跳到同一条重链上之后,查询一下两个点在这条重链上之间的点的信息就可以了。

只放树链剖分相关的代码:

(Tadd是线段树上的区间修改操作)

void add(LL x, LL y, LL z) {
	LL fx = top[x], fy = top[y];
	while (fx != fy) {
		if (dep[fx] < dep[fy]) {
			swap(fx, fy); swap(x, y);
		}
		Tadd(1, id[fx], id[x], z);
		x = fa[fx], fx = top[x]; 
	}
	if (id[x] > id[y]) swap(x, y);
	Tadd(1, id[x], id[y], z);
}

(Tquery是线段树上的区间查询操作,和区间修改基本一样)

LL query(LL x, LL y) {
	
	LL fx = top[x], fy = top[y];
	LL ret = 0;
	while (fx != fy) {
		if (dep[fx] < dep[fy]) {
			swap(fx, fy); swap(x, y);
		}
		ret += Tquery(1, id[fx], id[x]); ret %= p;
		x = fa[fx], fx = top[x];
	}
	if (id[x] > id[y]) swap(x, y);
	ret += Tquery(1, id[x], id[y]); ret %= p;
	return ret;
}

六、利用重编号的连续性,求某结点子树上的信息

这个就更简单了,由于一个结点的子树上,重编号(dfs序)是连续的,而我们在第一个dfs里面,也统计了这个子树的大小,那么我们可以直接用这个结点的重编号和子树的大小,直接作为左右端点,送到线段树里面去查询。

子树的区间修改:

void addt(LL x, LL z) {
	Tadd(1, id[x], id[x] + size[x] - 1, z % p);
}

子树的区间查询:

LL queryt(LL x) {
	LL ret = 0;
	ret += Tquery(1, id[x], id[x] + size[x] - 1); ret %= p;
	return ret;
}

AC代码

Codeforces

1624F - Interacdive Problem

交互题。可以想到是二分,但是不会一边二分一边移动。思考方向上受初始值的影响较大,看到求当前值的话,如果多往当前值这里想一想或许会节省不少的时间。

这里值得学习的地方是二分的边界问题。

学习的博客:浅谈二分边界问题

一般有这么几种写法:

1.记录答案法

while (l <= r) {
    int mid = (l + r) / 2;
    if (check(mid)) {
        ans = mid;
        r = mid - 1;
    }
    else l = mid + 1;
}
printf("%d", ans);

这个方法的跳出条件是l>r。当我们判断重点是否可行后,如果可行,我们就用ans记录该答案,然后让l跳过mid。这样可以有效避免死循环。

2.不记录法

while (l < r) {
    int mid = (l + r) / 2;
    if (check(mid)) r = mid;
    else l = mid + 1;
}
printf("%d", l);

上面这个代码是右侧可行版本的不记录法二分答案。即,如果这个mid不可行,说明这个边界一定不能取,就跳过去。

当我们计算mid的时候,我们的操作是计算(l+r)/2的下取整。当l+r为奇数时候,我们会产生两个中间数,在右侧可行版本中此时我们默认取下面的那个。如果我们取了上面较大的那个数的话,当r=l+1(此时l+r为奇数),且mid=(l+r+1)/2,check(mid)==0,那么我的操作r=mid之后,会发现,l和r的区间本身并没有在动,于是就陷入了一个死循环。

所以,右侧可行版本中,我们必须是取中间两个数中的较小数。

反之,左侧可行版本中,我们必须是取中间两个数中的较大数,才可以避免死循环。

下面是左侧可行版本的代码:

while (l < r) {
    int mid = (l + r + 1) / 2;
    if (check(mid)) l = mid;
    else r = mid - 1;
}
printf("%d", l);

最后的跳出一定是l==r,所以输出l和r都可以。

1.18

Nowcoder

小白月赛43

F - 全体集合

想到了到其中一个人的距离必须奇偶性相同,但是就没有下文了。

看了题解,感觉非常巧妙!这是一道二分图的题目。离散数学中有对二分图(也叫二部图)的一个介绍,那就是:二分图中不含奇数长度的环。通过这个性质,就可以从刚刚想到的那一点引申下来了。

如果这个图是二分图,那么一个点到另一个点的长度的奇偶性是固定的,因为从一侧到另一侧,不管走多少条边,走过的边数一定是奇数;从一侧走到同侧,不管走多少条边,走过的边数一定是偶数。

而如果这个图不是二分图,如果他没有环,那么他一定可以变成一棵树,通过染色,一定可以分成分明的奇偶性,将点划分成两个集合,形成二部图,所以如果没有环,就不可能不是二分图。(或者根据定义,不存在奇环的图是二分图,而没有环也算不存在奇环,所以是二分图)那么既然不是二分图,就一定含有奇环,而一旦含有奇环,就一定可以通过走奇环改变两点之间的距离奇偶性。所以只要不是二分图,一定可行。

首先我们判断是不是二分图。如果不是二分图,则一定成立。如果是二分图,则通过染色判断k个人所在的点是不是染的同一个颜色,如果是,则可行,否则不可行。

染色用dfs实现即可。判断不是二分图的条件:染色时遇到一条边,这条边的两个端点染了同样的颜色。

(大无语事件:第54行的读入本来应该是cin >> pos[i],一开始读成了cin >> pos[k],竟然过了60%的点。虽然这取决于数据的最后一个pos,但看到过了60%,我怎么也没想到是读入出错)

AC代码

Codeforces

1368B - Codeforces Subsequences

让每一个字母原位分裂,共同长大,是最优的。即,相同字母挨在一起,数量的最大值与最小值相差不超过1是最优的。一共有10个字母,每一个字母的数量是num[i],那么从左到右依次+1,观察这十个num[i]的乘积是否超过k,超过k则直接输出。

AC代码

1624E - Masha-forgetful

多做题目确实可以见多识广。往往cf的题目不能想的那么复杂,要灵活变通。这道题目,由于没有段数的限制,只有段数长度不能为1的限制,所以我们容易想到,其实我们只需要考虑长度为2和3的线段即可。由于n和m都不大,所以我们可以暴力存储所有出现过的长度为2和3的线段,然后读取目标数字串之后,我们用dp来记录当前这一位是否能够通过前面连续的串达到,大体查询方式和map的思路很相似。如果我们能够记录到最后一位,那么就一定是可行的,否则就是不可行的,输出-1。

当确认可行后,我们定义一个名为ans的vector,然后从最后一位往回找,如果选两个之后,下一个数字也可以被达到,那就选择长度为2的线段,然后直接跳转到下一个数字,否则就选长度为3的,这样一直选到最后一个为止,然后将vector逆序,再输出即可。

AC代码

1624G - MinOr Tree

求一个最小生成树,但是所谓最小,不是我们通常意义上的边权相加,而是边权相或。可以想到,我们选择的边如果出现1的位数越少,出现1的位越小,最后的结果就越小。我们没有办法通过找边的方式来求这条边,这时我们可以将思路反过来,利用二进制的性质,从高位到低位来看这个位置的1是否有必要。具体的方法就是将这个1先删掉,然后我们选择所有不包含这个1的边,看看这个图最后是不是连通的,如果连通,那么这个1就可以删掉,如果不连通,说明这个1不能删。

这种方法的正确性显然,因为我们在删高位的时候已经可以得出,删除掉的那些高位并不影响图的连通性,那么影响连通性的因素只可能是我们当前所删的这条边。而高位到低位,也恰好可以保证最后得到的可行答案是最小的。

AC代码

1.19

Codeforces

Codeforces Round #573 (Div.2)

本来是开的VP,但后来因为接电话和取快递,存在了时间上的空挡。所以就不标注通过时间了。

A - Tokitsukaze and Enhancement

对4取模后4种情况进行分类讨论即可。取模后结果相同的数答案相同。

AC代码

B - Tokitsukaze and Mahjong

分类讨论!在考虑牌型两种一样一种不一样的情况当中,忘记考虑点值相同的情况,导致WA了好多发!什么时候我才能把所有情况都考虑到呢?

AC代码

C - Tokitsukaze and Discard Items

很好想的题目,我觉得甚至比B题简单。把包含特殊页的最后一个格的后面插一个隔板,再用一个指针指向特殊点数组中最后一个被删除的点,然后从这个点开始往右爬,看摸到隔板为止,能移动几个格,那么隔板就往后移几位。考虑到n的数值很大,所以当我们隔板前面已经没有还没拿走的特殊点时,要通过计算一步到位,抵达下一个包含特殊点的页的最后一个元素的后面。这样的算法相当于只在特殊点数组上跑了,时间复杂度是O(m)的。

AC代码

D - Tokutsukaze, CSL and Stone Game

一直以为是博弈论的nim模型,所以一直想的是假思路,因为想到了最后的结果一定是单调递增的,但没有进一步想到是0~n-1,而且这里只能拿走一个,直接用抑或显然不妥当。

这道题目首先要想的是时津风的第一步的情况。我们不能保证时津风的第一步是必定成功的,所以要先考虑,什么情况下,时津风会在第一步输掉。

①如果时津风先手前,有三堆及以上的石头数量相等,那么时津风不管拿走哪一堆,都会有两堆数量相等,从而输掉。

②如果时津风先手前,有两堆数量是x个,有一堆数量是x-1个,那么时津风为了解决有x个的这两堆之间的矛盾,必定会从其中之一拿走石头,变成x-1个,然后x-1个石头的石堆变成了两个,从而输掉。

③如果时津风先手前,已经有两堆及以上的石堆石头数量是0个,那么不管时津风怎么操作都会输掉。

④如果时津风先手前,有两堆是x个,有两堆是y个,那么时津风肯定不能同时解决两个,只能解决其一而留下了另外两个相同数量石头的石堆,从而输掉。

如果发生以上四种情况的任意一种,则时津风必败。

否则,将石堆排序之后,从小到大,石堆所剩的石头一定是0~n-1。我们计算可以抽取的差值,这些剩余的石头,两个人按照怎么样的顺序拿都无所谓,看拿走最后一个可拿的石头的是时津风还是CLS,如果是时津风,那就是时津风胜,否则是CLS胜。
AC代码

E - Tokitsukaze and Duel

想法和题解基本一样,但可惜没能实现出来,可能是我思考的太复杂了吧。

首先很容易就可以想到,如果第一回合时津风没能赢下,在后续中,时津风就不可能赢了,因为quailty总有方法会使局面引向平局。所以先判断第一回合时津风可不可能赢。

如果时津风赢不了,那就看第二回合,quailty能不能必胜。我们要先让时津风在第一回合所执行的操作,尽可能让第二回合的quailty赢不了,做法就是让左右要么留0,要么留1。所以分两种情况讨论:

1.时津风第一回合先找到从左往右数第一个0,从这个0的后一个数开始,连续选k个数,使其全部变成1。然后看后面那段是不是还有0,如果有,则quailty一定不能赢,则平局。否则quailty必胜。

2.时津风第一回合先找到从左往右数第一个1,从这个1的后一个数开始,连续选k个数,使其全部变成0。然后看后面那段是不是还有1,如果有,则quailty一定不能赢,则平局。否则quailty必胜。

之所以必胜,是因为时津风已经尽可能让左右的两段0远了,而这样那边如果没有0,则说明没有办法把0分成不可能一次覆盖的两端,那不管自己怎么操作,quailty都会胜利。

如果左边右边一次性都可以覆盖住,是不是可以判断时津风胜利?

答案是可以,但这种情况在最开始就判完了,能进行到这里,说明这种情况不可能发生。

1.20

电脑终于修好了!上午取了回来,用测试软件测了测,终于回到了它应该有的性能!

不过我也不太敢用了,虽然它已经好了,但两个月就坏掉的笔记本,还是不太敢使劲用了(着实有点PTSD)

Codeforces

Codeforces Round #573 (Div.2)

F - Tokitsukaze and Strange Rectangle

也是让我收获满满的一道题目!

比较重要的就是关于“贡献”二字的理解。其实这道题目数种类数量的方式和之前一道题目范式几乎一模一样,即对于这个元素,他的种类是左边的元素数和右边的元素数的乘积。只不过那道题目是一维的,这道题目是二维的。

这道题目有一个突破口,那就是长方形是上端开口的,所以至少在纵向上,我们只需要一条线来分割,是可以O(n)枚举出来的,所以我们先让所有坐标按照纵坐标排序,然后自上而下考虑可能的矩形,由于我们的分割线是作为矩形的下边缘的,所以我们枚举的矩形自上而下。

对于每一个y,我们要再对坐标通过横坐标从小到大排列。然后我们从左往右看,对于这个y上的任意一个点来说,这个点对答案产生的贡献,就是看包含它的矩形,左边可以框的有多少种,右边可以框的有多少种,数出前面的点所有出现过的横坐标的数量,乘一起就行了。这里要注意的是,我们只能框住包含它在内的前i个点(假设这个点是排完序之后的第i个点),所以如果下一个点和它的深度相同,即y[i] = y[i + 1],那么我们往右框的时候,就不能框住下一个点了,到下一个点前面一个横坐标就结束了。

但这样的复杂度,如果我们在每一个y的每一个点都要从左到右查一遍x,这样的复杂度是相当不好的,达到了O(n^2),所以我们要考虑优化。优化首先考虑重复的操作,这里就是对于每一个点,我们都要从左往右找到前面的点出现过的横坐标的数量。而事实上,在前面的点查完之后,我们或许就应该可以通过一种方式把它记录下来。但为了满足我们的需要,我们要随时查询的区间是随着我们正在查询的点的横坐标变化而变化的,所以,我们遇到一个点,就记录他的横坐标出现过,然后区间查询前面的点的横坐标出现了多少种——单点修改,区间查询,好一个线段树!用线段树维护这些点的横坐标(需要离散化,因为坐标达到了1e9),叶子节点就是横坐标本身,这里要维护的就是横坐标有没有在前面出现过,也就是只有0和1两个状态,然后上面的父结点将这些0和1加起来,就是当前区间出现过的横坐标的数量了。这样,我们就将复杂度从O(n^2)降到了O(n\log n)了。

AC代码

Codeforces Round #490 (Div.3) (VP)

挑这一场单纯是因为这一场的比赛编号是999。

A - Mishka and Contest (0:09, +2)

A题+2!我以为只要是样例错了就不会有罚时,没想到扣了罚时,还1次扣20!

其实这不是最离谱的,最离谱的是我A题写错了!只能说,自己在写代码的时候注意力也没有集中起来,或者直戳本质:写代码的能力太差了。

AC代码

B - Reversing Encryption (0:22, +)

其实因为vector什么的用不太明白,导致这个题花了相当多的时间来完成。其实这个题n很小,就算是数组也可以轻松搞定。不过当时想用vector的reverse,但貌似看起来不太可行。

AC代码

C - Alphabetic Removals (0:32, +)

C题倒是正常发挥(?)。很常见也很老套的把每个字母对应的所有位置存储起来,到时候消除的时候直接变成O(1),优化时间复杂度,只不过这个题我用的是队列,虽然倒着处理vector也没问题。

AC代码

D - Equalize and Remainders (1:47, +2)

很感慨,当时看到自认为很好的思路MLE Test 5的时候,一度想摆烂了,结果后来想到一个思路,然后先TLE test 5,然后改了改A掉了。如果这个题没有A,那么想必排名还是3000+,但是由于这道题目过的人非常少,导致我过了这个题之后,变成4题250罚时,在rated的排名当中最后升到275,可以说是历史新高。就算在unrated里面,当前的排名是982,也是历史新高(从来没进过前1000)。

这道题目很重要的思路,写完之后会发现结果跟答案完全吻合的思路,就是,既然我们想要均等数量的余数的数组,那么我们先读入的直接进数组,统计好余数的数量,后进来的看看他的余数是多少,如果当前余数数量已经达到线了,就让他+1,看看+1之后的余数满没满,满了就落户,没满继续往后找。这样会TLE test 5(我就是这么T掉的)。

很容易想到一步优化,那就是我们直接记录当前余数满了之后下一个去找哪一个余数。最开始的时候,余数为i的下一个余数就是i+1,如果余数是m-1,下一个就是0,即形成了一个环,找下一个的时候,就沿着环找就行了。

但是沿着环一个个找,把下一个记录下来这个方式,也不是特别优,因为其实我们在查找下一个的时候,那些经过的已经满了的余数的下一个余数,其实就是我们正在寻找的余数,所以我们不仅要更新当前查询的余数的下一个余数,路径上经过的也要一并更新掉。这个很想我们在并查集路径压缩的写法。加上这个优化,就A掉了。

AC代码

(牛客的练习赛明天再补吧,只会第一题,菜得只想睡觉)

1.21

这个笔记本算是修不好了,今天莫名其妙就重启,我不知道是因为温度过高的自我保护系统还是因为其他原因,现在才下午两点,就已经突然重启了六次了。依旧很感慨,为什么才买了两个半月的电脑,用着像已经用了两年了?

Codeforces: Gym

101755B - Minimal Area

学到了三角形面积的新求法(也不能说学到,之前确实会,但做题的时候确实想不到这个方法。)

三角形的矢量叉积求法:

设三角形的三个顶点在平面直角坐标系中的坐标为(x_i, y_i)(x_j, y_j)(x_z, y_z)。我们随便选取一个顶点,假设顶点选(x_z, y_z),那么这个顶点所引出的三角形的两条边我们令其为向量\vec{a}\vec{b},则有\vec{a}=\left \{ x_i-x_z, y_i-y_z \right \}\vec{b}=\left \{ x_j-x_z, y_j-y_z \right \}

根据向量叉乘的几何意义,我们可知:

S=\frac{1}{2} \left | \vec{a} \times \vec{b} \right |=\frac{1}{2} \left | (x_i-x_z)(y_j-y_z)-(x_j-x_z)(y_i-y_z) \right |

在凸包当中,相邻三个顶点所围成的三角形的面积一定是最小的。

Nowcoder

牛客练习赛95

A - Duplicate Strings

记录初始字符串每个字符的出现次数,然后复制的时候就统计初始字符串被复制了多少次,然后问哪个字符就用哪个字符初始出现次数乘上初始字符串被复制的次数即可。

AC代码

B - Non-integer Area

比赛时就被这个题卡住了!面积公式用的是上面101755B的那个叉积面积公式。

显然,面积如果是小数,由于顶点坐标都是整数,所以小数点后面也只可能是.5或.0,用叉积面积公式的话,就是在原公式去掉1/2之后,如果是奇数,就符合题目的要求。首先统计所有点中横纵坐标奇偶性相同的6种坐标点各自的数量。那接下来就是统计\left | (x_i-x_z)(y_j-y_z)-(x_j-x_z)(y_i-y_z) \right |的奇偶性。求法就是枚举里面六个值的奇偶性,一共是2^6个,用0和1表示偶数和奇数,然后带进去看是奇数还是偶数,如果是奇数,就将枚举出来的三个点的横纵坐标奇偶性中,给出的点中出现过的次数,三个数相乘(用到最开始统计的那个数据),加到答案中去。最后,由于我们是暴力搜索,同样的三个点会被计算A^3_3=6次,所以将最终的结果除以6,就是最后的答案了。

AC代码​​​​​​

(巨丑无比的6个for循环嵌套在一起。。。) 

牛客小白月赛44

unr了...虽然就算不unr好像也没法上绿的样子。

A - 深渊水妖 (0:09, +)

按照题目的要求,就像我们自己数的方法一样,从左到右依次检查,遇到a[i] < a[i - 1]的时候,就查看这一段的最大差是多少,如果大于当前记录的最大差就更新,等于就添加答案,小于就直接忽略。由于我们是按照顺序检查的,添加的答案也一定是递增的,所以无需排序,直接输出答案即可。

AC代码

B - 顽皮恶魔 (0:17, +)

植物为1,不是植物的为0,提前记录所有保护伞的位置,将保护伞周围的八个格的点全转为0,最后统计1的数量即可。

AC代码

C - 绝命沙虫 (-4)

double的精度问题!因为这个,导致这道题目到比赛结束也没有过。这个取决于double的存储方式,在我们读入后就会产生一个误差,所以我们最好的方式,就是算完之后,取一个round,四舍五入之后就好了。

AC代码

D - 丛林木马 (0:39, +)

设第一个因数a的长度是la,第b的长度是lb,答案很显然是a \times lb+b \times la,答案很大,要一边算一边取模,所以用快速幂即可。

AC代码

E - 变异蛮牛 (1:23, +1)

dfs遍历整棵树,自下而上统计这个结点所在子树中有多少个点被标记为黑色的点,然后将黑点的答案加到一起,作为ans1。这个答案表示链上所有的点深度各不相同的链的数量。

然后对于每一个答案,我们看子结点的答案,然后将那些数两两相乘,求一个和。把每一个点的这个和都统计起来,加到一起,作为ans2。这个答案表示链上非端点的结点处于深度最小的位置的链,这个结点也是链的两个端点的最近公共祖先。

最后答案ans=ans1+ans2。

比赛的时候一开始ans2忘记算了,想了半天。

1.22

被拽去买衣服和鞋了,很累,学不进去,于是重新温习了Fate/stay night的UBW线的25集动画(当然是跳着看的),感觉心里很惭愧,混了一天……

Nowcoder

牛客小白月赛44

F - 幽暗统领

温习了树的重心:一棵树当中,先统计所有结点的各个子树的大小(包括父结点那一侧的子树),然后每一个结点记录其所有子树中最大的那棵子树的大小。比较所有结点的这个信息,这个信息数值最小的点,就是这棵树的重心。

来看这道题目:

画几个例子之后可以发现,如果所有链当中最长的那条的长度小于或等于结点总数的一半,那么所有结点都有机会成为重心(把那个结点使劲往中间塞,绝对是有机会成为重心的)。而当最长的那条的长度大于一半时,只有这条链上的结点有机会成为重心(其他链的结点必然有一棵子树包含这条最长的链,他们的最大子树的大小必然大于总结点数量的一半,这与树的重心的性质相违背)。

所以我们分类讨论:如果最长的链长度小于等于结点总数的一半,直接输出总结点数。

反之,我们要探求长链上最左端和最右端可能成为重心的点。我们考虑贪心,将剩余所有短链接到长链的左端,形成一个大整长链,取中间的点就是最左端的可能成为重心的点。把所有的接到右边,同理得到最右端可能成为重心的点。

我们可以认为,在两种极端情况之间,我们通过调节短链所衔接的位置,可以让最左端和最右端的可能成为重心的点之间的所有的点,都有机会成为重心。所以,最后的答案就是最右侧的点-最左侧的点+1。剩下的就是推推式子就可以得出来以下结论:

令结点总数为sum,长链长度为l,答案为ans,则

1.若l\leq \frac{sum}{2},则ans=sum

2.若l>\frac{sum}{2}sum为奇数,则ans=sum-l+1

3.若l>\frac{sum}{2}sum为偶数,则ans=sum-l+2

AC代码

Codeforces

Codeforces Round #767 (Div. 2)

A - Download More RAM (0:05, +)

小贪心。需求小的一定比需求大的先执行。

AC代码

B - GCD Arrays (0:17, +)

考虑到相邻两个数的gcd是1,可以想到最优的方法是把奇数全部并到偶数里面去。有一些需要考虑到的细节:

①如果l=r=1,则为NO,因为无论如何,gcd都不可能大于1。

②如果l=r>1,则为YES,因为只有一个数,gcd是它自身,已经大于1了。

③如果r-l+1是偶数,则当k<(r-l+1)/2时为NO,否则为YES。因为为每一个奇数都可以配一个偶数,一共可以配(r-l+1)/2组,只要把这些组配对好就可以了,否则gcd一定为1。

④如果r-l+1是奇数,则当奇数多于偶数且k<(r-l+1)/2+1,或当偶数多于奇数且k<(r-l+1)/2时为NO,否则为YES。与③同理。

C - Meximum Array (1:19, +)

我比赛时候的方法非常麻烦。我用pd_ds的ordered_set来维护两件事:一个是剩余数组的最大值,一个是剩余数组中已经没有了的数的最小值是多少。从左到右遍历数组,先统计当前我们可能遇到的最大值,这里的情况比较复杂。要考虑最大值的同时还要考虑mex本身的最大值可以达到多少,因为有些数后面直接就没有了,所以mex本身是有上限的,通过ordered_set来获得这个最大值,然后遇到后,把最大值+1加到b数组,循环往复。这道题做了一个多小时,想到这个方法倒是很快,但是花了好多时间去写和调试。真心觉得我的代码能力很弱。

但是赛后看到题解后,我觉得我自己就是个zz。其中一个ordered_set维护的剩余数组的最大值完全是没有必要的东西,完全可以直接从后往前遍历一遍就可以获得所有元素的后缀的最大值。而且考虑遍历断点,应该是考虑当前mex与整个数组的mex,而不是整个数组的最大值,mex这个东西也是可以从后往前遍历数组就可以得到的。

唉!

AC代码 (in contest)

AC代码 (after contest)

D - Peculiar Movie Preference (1:40, +)

做过CF1624E - Masha-forgetful的话,那么这道题就会感到无比亲切。这道题目,我们要寻找一个方式拼成一个回文串。这里回文串的长度并没有给要求,所以使这道题目变得非常好做。

如果有长度为1的字符串,那么由于其自身就是回文串,所以直接OK。

如果有长度为2且两个字符都相等,或长度为3且三个字符都相等,那么由于其自身就是回文串,所以直接OK。

接下来我们就要记录已经读入的字符串中出现过的片段:

假如我们读到了长度为2的字符串s,我们用d2记录,令d2[s[0]-'a'][s[1]-'a']=1。

假如我们读到了长度为3的字符串,那么要记录两个东西:令d3[s[0]-'a'][s[1]-'a'][s[2]-'a']=1,da[s[0]-'a'][s[1]-'a']=1。

记录前两个字母的原因是,假如当前读到的字符串是abc,如果后面我读到了ba,那么也可以构成串。

这样当我们读到长度为2的字符串时,我们只需要检查d2[s[1]-'a'][s[0]-'a']和da[s[1]-'a'][s[0]-'a']是否存在值为真即可,如果有一个为真,直接OK。

当我们读到长度为3的字符串时,我们只需要检查d2[s[3]-'a'][s[2]-'a']和d3[s[3]-'a'][s[2]-'a'][s[1]-'a']是否为真即可。如果有一个为真,直接OK。

AC代码

1.23

Codeforces

Codeforces Round #767 (Div. 2)

E - Grid Xor

可以说是想了好久,看题解也没太看明白。实质上,就是找一个填法,选择一个格子就操作其上下左右四个格,如果原来是暗的就点亮,如果原来是点亮的那就灭掉。

在评论区看到了一个非常厉害的做法:

 我们选择红格子的进行操作之后,点亮的就是黄格子。我们惊讶地发现,正好有规律且均等地点亮了一半的格子。我们再反过来操作一次,就可以点亮剩余的一半。

现在的问题就是如何找到红格子的位置:可以发现规律,只观察一圈的边缘格,可以发现边缘上面的红格子相差都是3个格子,然后从这个格子向左下方向,间隔一个方块延申。所以我们只需要在上边缘和右边缘找红格子,然后向左下延申即可。

太妙了,这个做法!

AC代码

F1 - Game on Sum (Easy Version)

压根就没往dp上去想……这种题目做的还是太少了。

先考虑一个简单的问题:当n=2,m=1时,情况是怎样的。

我们设Alice第一回合出的数是x。在这种情况下,假设x很小,那么Bob会选择用掉仅有的一次add。如果x很大,Bob一定会选择不用add,而是用掉仅有的一次minus。这是只知道当前数字下,Bob能够做出的最好的决定。

如果Bob在此时用掉了add,那么说明下一次他一定会用minus,那么Alice只有出0才能让分数尽可能大。如果Bob在此时没用add,那么下一次他一定会用add,为了让分数尽可能大,Alice会出k

所以,最后的分数就是xk-x。两个人所希望的分数的走势相反,所以我们可以感性认为,他们所追求极致后所达到的位置,一定是中间位置。因为无论如何,作为先手的Alice没有可以参考的数据,而Bob有着可以看着Alice的数字进行选择的优势,所以Alice不冒险的话,会选择这两种分数的临界状态,也就是x=k-x,即x=\frac{k}{2}

综上,当n=2,m=1时,Alice会选择x=\frac{k}{2}

我们现在把这个问题扩展到我们这道题目上来,n个回合,Bob至少使用m次add,两人都追求极致后分数是多少?

我们可以把这个问题拆成子问题,即在第n回合时,Alice会选择怎样的数字?Bob会add还是minus?我们把注意力放到第n个回合上,设这一回合Alice选择的数是x,这回合之前的分数是s

如果x很大,那么Bob会选择minus操作,那么得分就是s_1-x

如果x很小,那么Bob会选择用一次add操作,那么得分就是s_2+x

这里区分了s,因为Bob的选择不同,那么意味着转移之前的状态所产生的分数也不同。

而第n回合最后的分数到底是多少,是取决于Bob的选择的,Bob想要让分数变得更小,所以最后一定会取最小的那个答案。

这里就很有dp的味道。如果我们设dp[i][j]表示n=i,m=j时最后的分数,那么我们可以进行如下转移:

dp[i][j] = min(dp[i-1][j-1] + x, dp[i-1][j] - x)

和刚刚n=2,m=1时的道理一样,Alice会选择对自己有利的数字,而Bob会选择对自己有利的操作。所以两个人的力是往中间使的。所以最后的结果,会趋近于两个趋势之间,即可以认为:

dp[i][j] = (dp[i-1][j-1] + dp[i-1][j]) / 2

这就是最后的转移方程了。除法用逆元替换一下即可。

AC代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值