2020双周训练训练日志

第 3 次双周训练

VP

A - 6789

        签到题,按照题意模拟就行了,由于 lincong 将题目中的“中心对称”看成了“轴对称”,导致 WA 了三发还不知道为啥,之后重读题目才发现了这个锅,修好之后就 A 了。

I - Minimum Diameter Spanning Tree

        I 题的题意很好理解,求给定图的最小直径生成树。

        问题的关键在于求出图的最小直径,换句话说,就是要求出一个点,使得它到图中各点距离的最大值最小,且这个点可以在一条边的中间。类比于树的中心,我们可以称这个点为图的中心,简称“图心”。

        一个十分显然的思路是枚举每一条边,看“图心”是否在这条边上。

        具体来说,对于一条边 (u, v),我们假设“图心”就在这条边上,下一步是选择“图心”的位置。由于“图心”的可选位置有无数多个(可以从 u 一直过渡到 v),所以我们可以把图中某一个点到“图心”的距离,想象成一个函数 f(x),自变量 x 是“图心”到 u(或 v)的距离。而 f(x) 的图像就是一条折线段,它的最大值在 [0, w](w 指边权)中的某处取到。

        这样,图中各点到“图心”的距离的函数图像,形成了一个个折线段。如果我们把这些折线段全部叠加到一起,并取此时图像中最上方形成的一条折线段(见下图),那么这条折线段,就是“图心”到图中各点距离的最大值。

        这时,我们只需要找到这条折线段中最低的交点,那么这个交点的横坐标,就是“图心”所在的位置。因为选取这个点作为“图心”后,它到图中各点距离的最大值最小,实现了我们一开始提出的目标。

        那么,应该如何寻找这个最低的交点呢?

        首先,我们需要过滤一些没有用的点。什么叫做没有用的点呢?观察下图:

        最上方的红色折线段就是我们需要的折线段,显然,它是由 3、4、5 这 3 条折线段相交构成的,所以 1、2 这两个点属于没有用的点。

        如果记两点之间距离为 dis(i, j),那么对于1号点来说,因为 dis(1, u) < dis(3, u),dis(1, v) < dis(3, v),所以 1 号点形成的折线段,完全处于 3 号点形成的折线段的下方(易知折线段斜率相同),因此 1 号点是一个没有用的点。对于 2 号点同理。

        当我们去除掉没有用的点之后,图像变成了这个样子:

        根据观察,我们发现了一个很有用的性质:如果 dis(i, u) 是按照升序排列的,那么 dis(i, v) 一定是按照降序排列的(由于折线段斜率相同)。因此,这些剩下的折线段之间的交点,一定是按照 dis(i, u)(或 dis(i, v))排好序后,相邻的两条折线段相交出来的!这下最低的交点对应的距离 ans 就迎刃而解了。

        按照这个方法,我们可以求出每一条边对应的 ans,而最小的 ans 所对应的那条边,就是这幅图真正的“图心”所在的边。以这条边作为起点生成的树,就是我们要求的最小直径生成树,最小直径就等于 ans * 2。

        有了思路之后,代码实现并不是很难,经过几次 WA 之后,lincong 发现并修正了一些小错误(如数组越界、精度问题、int 溢出等),最后终于在 2.5h 左右的时候 A 了这道题。

H - Maximizer

        H 题的题意也不难理解,给定两个长度为 n、元素为 1 ~ n 的序列,每次只能交换其中一个序列的相邻两项,求最小交换次数,使得这两个序列对应项的差值的总和最大。

        设不动的序列为 a 序列,交换的序列为 b 序列。1mszs 在短暂的思考之后,认为只需把 b 序列中小于 mid(中值)的数看作 1,大于 mid 的数看作 2,让 b 序列中的 1 都移动到 a 序列中 2 的位置、b 序列中的 2 都移动到 a 序列中 1 的位置即可。

        具体来说,只需要将 b 序列中的 1 进行移动,只要所有的 1 都移动到了指定位置,那么所有的 2 自然也移动到了指定位置。

        而对于 1 的移动,只需要从前到后依次在 a 序列中找 2,在 b 序列中找 1,找到一组后,它们俩的位置之差就是需要移动的次数。将它们俩的位置之差加入 sum,然后接着向后移动,重复以上操作,直到全部查找完毕,此时的 sum 就是题目要求的最小移动次数。

        代码实现起来非常简单,思路也感觉没什么问题,但是交上去之后却发现 WA on test 7。查了半天也没查出来错误的 1mszs 把代码扔给了 lincong,然后自己去看 G 题。

        lincong 接手 H 题之后,想了一会儿,感觉这个思路没有毛病,应该是某个奇怪的地方写挂了。果不其然,在处理 n 为奇数的情况时,1mszs 忘记给查找数组用的指针清零了,修正了之后 AC。

G - Lexicographically Minimum Walk

        G 题的题意是这样:给定一幅有向图,每一条边有一个边权,且边权之间互不相同。求从s到t的边权序列中,字典序最小的那个序列。若不存在 s 到 t 的通路,输出 IMPOSSIBLE;若字典序最小的序列长度超过 1e100,输出 TOO LONG;否则输出那个序列。

        A 掉 A 题的 lincong 看了一会 G 题,感觉没有思路,于是滚去看 I 题。

        把 H 题代码扔给 lincong 的 1mszs 接手了 G 题,首先发现这题是一个贪心,每次能走就选择边权最小的边去走(前提是下一个点是可以到达终点的点)。至于判断一个点是否是可以到达终点的点,只需反向建图,从终点 dfs 一遍即可。

        其次 1mszs 注意到,输出 TOO LONG 的情况,一定是按照贪心的策略走的时候,碰到了环,那么就会这么无限循环下去。

        有了思路的 1mszs 开始写代码,不一会儿就写完并调试好了,但是交上去之后却发现 WA on test 6。查了半天也没查出来错误的 1mszs 把代码扔给了 lincong,然后自己去看 F 题(似曾相识的经历)。

        lincong 接手 G 题之后,感觉应该又是某个奇怪的地方写挂了。在手动模拟了一组数据之后,发现 1mszs 忘记处理终点包含在环内的情况了,在这种情况下仍然输出了 TOO LONG,修正了之后 AC。

J - Parklife

        时间已经过去了 3.5h,1mszs 和 lincong 都略感疲惫,但是题还是得继续往下看。

        在观察了每道题的通过人数之后,1mszs 和 lincong 认为剩下的题中,只有 F 题和 J 题可以尝试一做。于是看完了这两道题的题意之后,1mszs 打算去研究 F 题,而 lincong 决定去研究 J 题。

        J 题的题面比较冗长,大意如下:

        给定一条凸弧,弧上有许多等距离的点。在弧的下方,有 n 条连接着弧上两点的线段,且线段之间两两互不交叉。每一条线段有一个权值。题目的要求是选定其中的一些线段,使得从弧的每个点向下看,最多都只有k条线段被选中,且被选中的线段的权值之和最大。对于每一个 k(1 ~ n),输出最大的权值之和。

        由于线段之间两两互不交叉,所以我们可以类比线段树,把这些线段全部建到一棵树上(令子节点的区间包含在父节点的区间中)。这样在 dfs 的时候,我们就可以把子树的权值,不断地合并到父亲节点上。

        具体来说,我们需要对每一个节点开一个优先队列,用来存放当前节点包含的区间 [l, r],所能合并出来的所有权值(叶节点的优先队列中就只有本身的权值)。那么只需在 dfs 回溯的时候,把子节点的优先队列上的信息,全部并入父节点的优先队列即可(按秩合并)。

        这样的话,在根节点的优先队列中,就存放着合并一层所能得到的所有权值(且每个权值都是当前最优的选择)。查询的时候,只需要从根节点的优先队列中不断取出队首,就能得到合并 k层所能得到的最大权值了。

        举个例子:

        6
        1 2 10
        2 3 10
        1 3 21
        3 4 10
        4 5 10
        3 5 19

        将题目中给出的这些线段按照读入顺序编号(根节点编号为 n + 1),然后把它们建到树上后,是这个样子:

        在 dfs 的时候,我们首先搜到了 1 号节点,是一个叶子节点,所以直接将 10 并入 3 号节点的优先队列。接着搜到了 2 号节点,同样把 10 并入 3 号节点的优先队列,3 号节点的优先队列变成了 [20]。最后回溯到 3 号节点,将自己的权值加入优先队列,此时 3 号节点的优先队列变成了 [21, 20]。对于 6 号节点同理。

        当回溯到 7 号节点时,我们取出 3 号优先队列和 6 号优先队列的队首,合并,然后加入 7 号优先队列,这时就得到了合并一层所能得到的最大权值 41。同理,得到了 39。

        代码实现起来难度也不算太大,最后在 4h 左右的时候通过了这题。

        然而,天有不测风云,正当 1mszs 和 lincong 讨论 F 题的时候,一帮不速之客闯入了研讨室。原来,是 1mszs 预约的研讨室到期了。因此,1mszs 和 lincong 迅速收拾东西,草草结束了本次 VP。

补题

F - Hilbert's Hotel

        应该是个线段树,不过目前一直卡在 WA on test 20,没有进展。

第 4 次双周训练

补题

        1mszs 补了 ABDHM 题,lincong 补了 GIJKL 题。

A - Artwork

        在一个 m × n 的矩形房间里,有一个盗贼在 (0, 0) 处。房间内有 K 个摄像头,给出每个摄像头的位置和监控的半径,盗贼不能进入任何摄像头监控的范围,包括边缘。问这个盗贼是否能够到达 (m, n)。

        签到题,用并查集合并圆,最后查询一下有没有哪个集合,能同时封住上右、下左、上下、左右中的一种或多种,有的话就不能,没有就能。

B - Buffoon

        签到题。

D - Denouncing Mafia

        给出一棵有 n 个节点,根节点为 1 的树,每一次操作可以选择一个节点,然后将这个节点到根节点路径上的每一个点染黑。求进行 k 次操作后,最多能把多少点染黑。

        显然每次选择叶节点最优,只需确定每个点是属于哪个叶节点向上的链即可。

        设 len[x] 是以 x 为根节点的最长链的长度。在 dfs 回溯的时候,选 x 的儿子中 len 最长的,接到 x 上,把其他儿子的 len 加入优先队列,意味着它们这些链到此为止了。最后 ans 就是 len[1] 加上优先队列中的前 k - 1 个。

G - Getting Confidence

        有 n 个物品,要把他们放回到 n 个位置,第 i 个物品放回到第 j 个位置有 a[i][j] 的确信度。求一种放法,使得 n 个物品的确信度乘积最大。

        显然,n 个物品与 n 个位置构成了一张二分图。

        由于 log(ab) = log(a) + log(b),因此我们可以用 log 将确信度乘积最大,转换为确信度的和最大,然后就是一道裸的二分图最大权匹配了。

H-Hour for a Run

        签到题。

I-Interplanetary

        给定一幅有 n 个点的图,每个点有一个点权。

        给出 q 次询问,每次询问包括起点、终点和限制条件,限制条件为:从起点到终点的路径上,每个中间点(不包括起点和终点)的点权必须是最低(高)的k种点权之一。问在此限制条件下,能否从起点到达终点,能的话求出最短路,不能输出 -1。

        首先,因为询问是离线的,所以我们可以将这 q 个询问按限制分类并排序,保证限制的数量是单调递增的。

        接着,我们再把点权从小到大(从大到小)排序,这样的话,我们就可以随着限制数量的不断增加,而不断加入新点。每次加入新点,我们都需要以这个点作为中转点,跑一次 floyd,来更新最短路。

        最后,我们再按照询问的顺序,依次输出 ans 即可。

J-Jar of Water Game

        按题意模拟即可。

K-Keep Calm and Sell Balloons

        给定一个 2 × n 的网格,从任意格子出发,不重复地遍历每个方格,遍历方法为八联通,求不同的遍历方法数。

        模拟 + 思考后,得出递推公式:f(n) = 4 × a(n) + 2 × c(n)。

        其中:

        a(n) = 2 × a(n - 1) + 4 × a(n - 2) + b(n - 1),a(1) = 1,a(2) = 6。

        b(n) = 2 × b(n - 1),b(1) = 2,b(2) = 4。

        c(n) = 8 × a(n - 2) + c(n - 1),c(1) = c(2) = 0。

        由于 n ≤ 1e9,所以要用矩阵快速幂加速数列递推。

L-Less Coin Tosses

        打表找规律,ans 就是 2 的 (n 在 2 进制下 1 的个数) 次方。

M-Maratona Brasileira de Popcorn

        签到题,二分答案。

第 5 次双周训练

VP

I-Absolute Game

        签到题,按照题意模拟即可。

        对于后手而言,不论先手最后剩下哪个数字,他都会让自己剩下的数字,与先手剩下的数字相差最小。

        对于先手而言,因为他也知道,后手会使他剩下的数字与后手剩下的数字相差最小,所以他只能尽量使这个最小的相差最大化。

        具体来说,对于 a 数列中的每个数字,只需枚举 b 数列中的所有数字,找到相差最小的一对,记它们的差为 c,那么所有的 c 中最大的一个,就是答案。

D-Cycle String?

        签到题,只需考虑某一种字母数量超过 n/2 的情况即可。对于这种情况,当其他字母数量之和 > 2 时,可以构造出题目要求的字符串,否则不能。

J-Graph and Cycles

        给定一个有 n 个顶点的无向完全图(n 为奇数),每一条边有一个边权。题目的要求是将这个图分为多个环(每条边只能且必须使用一次),记环内相邻两条边的边权为 u、v,使得所有的 max(u, v) 之和最小。

        直接构造很难下手,于是分析题目条件。

        由于 n 为奇数,不难发现所有点的度数为偶数,而一个环中相邻的两条边又是通过一个公共点相连接的。因此,我们可以将每个点连接的这 (n - 1) 条边分为 (n - 1) / 2 对,不必考虑每一对边具体是属于哪一个环的,只需要让每一对边的 max 值之和最小即可。

        那么,我们对每个点连接的边按边权排序,取相邻的两条边构成一对即可,时间复杂度 O(n^2log_2n)

B - Level Up

        B 题的题面比较冗长,大意是你有两个等级,给你 n 个任务,每个任务在 1 级和 2 级下做的时候,花费的时间和得到的经验值是不同的。当 1 级的经验值满了之后就会升级,升级后只能在 2 级下做任务。并且 1 级的经验值溢出后,可以加到 2 级的经验值上。问最少花费多长时间,才能使 1、2 级的经验值均满。

        显然,这是个 01 背包问题,分别考虑每个物品对 1 级和 2 级的贡献即可。一个细节是需要对读进来的数据按照 x(在 1 级下做任务得到的经验值)从小到大排序,这样可以尽量多的将经验溢出给 2 级。

        设 dp[j][k] 为:1 级经验为 j、2 级经验为 k 时,花费的最少时间。受题意影响,lincong 在写dp 转移方程的时候,误以为只有 j >= s1(1 级升级所需的经验值)时,才能使用一个任务在 2 级下做的时间和经验。因此写出了如下错误代码:

for(int i=1;i<=n;i++)//枚举每个任务
    for(int j=s1;j>=0;j--)
    	for(int k=s2;k>=0;k--)
    	{
    		int nj=min(s1,j+a[i].x);
    		int nk1=min(s2,k+max(j+a[i].x-s1,0));//计算溢出
    		int nk2=min(s2,k+a[i].y);
    		dp[nj][nk1]=min(dp[nj][nk1],dp[j][k]+a[i].t);//当前任务在1级下做
    		if(j>=s1)
				dp[j][nk2]=min(dp[j][nk2],dp[j][k]+a[i].r);//当前任务在2级下做
		}

        然而事实是,我们可以在任何时候使用一个任务在 2 级下做的时间和经验。

        原因是:在 1 级经验不满 s1 时,我们使用一个任务在 2 级下做的时间和经验,可以理解为把这个任务寄存到 2 级下,当 1 级经验足够 s1 之后,立刻去做这个任务即可。

        反而是当 j >= s1 时,我们不能使用一个任务在 1 级下做的时间和经验,因为这时已经升级到 2 级,只能在 2 级下做任务了。正确代码如下:

for(int i=1;i<=n;i++)//枚举每个任务
	for(int j=s1;j>=0;j--)
		for(int k=s2;k>=0;k--)
		{
			int nj=min(s1,j+a[i].x);
			int nk1=min(s2,k+max(j+a[i].x-s1,0));//计算溢出
			int nk2=min(s2,k+a[i].y);
			if(j<s1)
				dp[nj][nk1]=min(dp[nj][nk1],dp[j][k]+a[i].t);//当前任务在1级下做
			dp[j][nk2]=min(dp[j][nk2],dp[j][k]+a[i].r);//当前任务在2级下做(j<s1时可认为是寄存到2级下)
		}

        因此 WA 了 11 发之后成功 AC 此题。

补题

        1mszs 补了 G题,lincong 补了 AEF 题。

G - Projection

        纯模拟。其实 VP 时,1mszs 已经基本想出此题的正确做法了,由于时间不够等不可抗因素,未能 AC 此题。

        G 题的题意是这样:给你一个侧面投影和一个正面投影,让你判断这两个投影能否组成一个立体结构。如果能的话,输出组成它的小方块个数的最大值,以及它们的位置;接着输出小方块个数的最小值,以及它们的位置(输出字典序最小的一种方案)。如果不能的话,输出 -1。

        最大值很好处理,只要是能放小方块的地方,全部放上即可。

        最小值也不算难,我们按层去放小方块。首先,我们需要计算出这一层的 x 轴和 y 轴上分别有多少个阴影。对于 x、y 数量相同的情况,我们 1 对 1 依次去放即可;对于 x、y 数量不等的情况,我们让多出来的部分全部放在第 1 行(列),剩余的 1 对 1 依次去放即可。

        VP 结束后,1mszs 继续完善赛场上的代码,WA 了 3 发之后 AC。

F - Game on a Tree

        VP 时,以为 F 题是树上博弈论之类的奇奇怪怪的东西,于是没敢写。VP 结束后,发现就是个树上最大匹配罢了。若最大匹配能覆盖所有点,则 Bob 胜;否则 Alice 胜。

        代码实现也很简单,在树上跑一遍 dfs 即可。若回溯的时候,x 的子树中存在没有匹配的点,就用自己匹配它;否则的话,x 成为一个没有匹配的点。具体看代码:

int dfs(int x,int fa)
{
	int siz=0;//siz为x的子树中没有匹配的点的数量
	for(int i=head[x];i;i=e[i].nex)
		if(e[i].to!=fa)
			siz+=dfs(e[i].to,x);
	if(siz)
		siz--;//子树中存在没有匹配的点,用自己匹配它,没有匹配的点的数量减一
	else
		siz=1;//子树中不存在没有匹配的点,于是自己成为一个没有匹配的点
	return siz;
}

A - Max or Min

        题意:有 n 个数呈环形排列,每次操作可以修改其中的一个数,将它变成它和它相邻的两个数字中的最大值或最小值,求最少操作次数,使得这 n 个数都变成同一个数 k。对于 1 ~ m 的每个整数 k,输出最少操作次数。

        显然,若这 n 个数中没有 k 的话,我们不可能将这 n 个数都变成 k。而当这 n 个数中存在 k 的话,我们一定可以通过有限次数将这 n 个数都变成 k。

        首先,我们可以把所有小于 k 的数字看作 -1,所有等于 k 的数字看作 0,所有大于 k 的数字看作 1。而我们的任务就是将所有的 -1 和 1 修改为 0。

        考虑这样一种情况:序列中没有 1 和 -1 的交替。这时操作次数就是 1 和 -1 的个数。

        比较麻烦的情况是,序列中存在 1 和 -1 的交替,那么我们需要通过额外的操作次数,将这样的交替序列抹除。记交替序列的长度为 len,那么显然我们需要至少 len/2 次操作,来将所有的 1(-1) 变成 -1(1) ,之后就变成了上面的情况。

        于是问题转换成了:求序列中 1 和 -1 的交替序列的个数及长度。

        由于查询是单调的,所以当一个数被看作 -1 后,它将不会再被改变。考虑到这一点,我们只需实现单点修改操作,那么开一个线段树记录交错序列长度,每次单点修改更新即可。

        其中,合并两个子区间的代码如下:

node merge(node& lc,node& rc,int l,int r)
{
	tree fa;
	int mid=(l+r)>>1;
	if(b[mid]&&b[mid+1]&&b[mid]+b[mid+1]==0)
	{
		if(lc.len1==mid-l+1)
			fa.len1=lc.len1+rc.len1;
		else
			fa.len1=lc.len1;
		if(rc.len2==r-mid)
			fa.len2=lc.len2+rc.len2;
		else
			fa.len2=rc.len2;
		fa.ans=lc.ans+rc.ans-(lc.len2/2+rc.len1/2)+(lc.len2+rc.len1)/2;
	}
	else
	{
		fa.len1=lc.len1;
		fa.len2=rc.len2;
		fa.ans=lc.ans+rc.ans;
	}
	return fa;
}

E-Life Transfer

        按题意枚举即可。从全部是摩托车的情况,枚举到全部是汽车的情况。

        具体见代码:

while(r-l+1>=k)
{
	if(a[r]+d<lc)
		break;
	car++;
	if(a[r]<lc)
		cost+=(lc-a[r]);
	if(a[r]<lm)
		cost-=(lm-a[r]);
	else
		rest-=min(d,a[r]-lm);
	for(int i=l;i<l+k-1;i++)
	{
		if(a[i]+d<lm)
			motor--;
		else if(a[i]<lm)
			cost-=(lm-a[i]);
		rest+=min(d,a[i]-1);
	}
	if(rest>=cost&&motor==0)
		ans=min(ans,car*pc+(n-car*k)*pm+cost*t);
	l+=k-1,r--;
}

第 6 次双周训练

补题

        1mszs 补了 CHI 题,lincong 补了 BDGJ 题。

B - Bracelets of Orz Pandas

        考场上感觉是个组合数学,但是推了好久式子也没推出来,浪费了好多时间。结束之后,发现这题要用到没学过的知识——杜教筛,也算涨了一波姿势。

        其中,(带记忆化的)杜教筛的代码如下:

ll DuJiaoShai(ll x)
{
	if(x<=maxp)
		return s[x];
	if(d[x])
		return d[x];
	ll ans=x*(x+1)/2;
	for(ll i=2,j;i<=x;i=j+1)
	{
		j=x/(x/i);
		ans-=(j-i+1)*dj(x/i);
	}
	return d[x]=ans;
}

J-Junction of Orz Pandas

        容斥原理,考虑对 L 型计数,式子很长,就不在这里放了。

D-Data Structure Master and Orz Pandas

        推式子 ++。

G-Gery's Problem and Orz Pandas

        一个比较麻烦的 LCA,要求的式子被拆成了很多项,分别计算即可。

H-Hamming Code and Orz Pandas

        比较简单的思维题,看懂题意暴力即可。

I-Irregular Shape of Orz Pandas

        计算几何,求多边形面积,分割成一个个三角形分别求即可。由于精度问题,需要用 long long 存储答案,最后再特判奇偶即可。

        update: 注意处理多组数据,因为这点 WA 了一天的 lincong 如是说。

C-Closestools of Orz Pandas

        set 暴力维护两个操作即可。

第 7 次双周训练

VP

D - Bits Reverse

        给你两个数字 x、y,每次可以对 x 在 2 进制下,任意的相邻的 3 个位置进行操作,使得中间的位置上的数字不变,两侧的两个位置上的数字交换。

        问最少操作多少次,可以使得 x 变成 y。

        签到题,对奇数位和偶数位分类讨论即可。若 x 和 y 的奇数位和偶数位上1的数量不同,则无解;否则一定有解,且 ans 就是 1 的位置之差。

        1mszs 在计算 ans 的时候,枚举偶数位时,手误打成了 odd.size(),然后 WA 了一发不知道为啥。lincong 接手代码之后,找到问题并修正后 AC。

J - stone Game

        有 n 堆石子,保证相邻两堆不等。A 和 B 玩游戏,轮流从这 n 堆中,任意拿走一颗石子,但需要保证拿走第 k 堆的一颗石子后,第 k 堆的石子数量不能和它相邻的两堆相等。无法拿石子者输,问谁能赢?

        显然,石子数量的单调性不会发生改变,所以能拿走的石子数量就是:保证序列单调性不变的前提下,尽量多的减少序列的值。

        那么,我们只需找到序列的极小值和极大值的位置,先将所有的极小值都设为 0,然后从极小值的位置开始向两边走,令每一个数等于前一个数 +1,这样就可以在保证序列单调性不变的前提下,使得序列的值最小。

        特殊地,极大值的位置会被走到 2 次,那么将极大值设为两次走到时的最大值即可。

        最后,我们只需计算一下修改后的数列与原先的数列的差,这个差值就是能拿走的石子数 num。如果 num 是奇数,则 A 赢,否则 B 赢。

G - Greatest Common Divisor

        给你 n 个数,每次操作可以让这 n 个数 +1,问最少操作多少次,可以使得这 n 个数的 gcd > 1。

        注意到,每次操作不会改变这 n 个数之间的差值,因此如果这 n 个数的gcd > 1,那么它们的差值的 gcd 也必然 > 1。

        因此,我们只需计算一下这 n 个数的差值的 gcd,若 gcd 等于1,则无解(这 n 个数都等于 1 时除外);若 gcd > 1,就枚举这 n 个数,用 gcd 的因子来计算操作次数,找到最小的那个即可。

        代码如下:

ll ans=2e15,tot=0;
for(ll i=2;i*i<=p;i++)//p是这n个数的差值的gcd
	if(p%i==0)
		ys[++tot]=i,ys[++tot]=p/i;//ys数组记录gcd的因数
if(!tot)
	ys[++tot]=p;
for(int i=0;i<a.size();i++)//枚举这n个数
	for(int j=1;j<=tot;j++)//枚举每一个因数
	{
		if(a[i]%ys[j]==0)
			ans=0;
		else
			ans=min(ans,ys[j]-a[i]%ys[j]);//计算操作次数
	}

        特殊地,当这 n 个数全部相等时,如果这 n 个数都等于1,那么操作 1 次即可;如果这 n 个数都 > 1,那么无需任何操作。

        由于 H 题从头调到尾都没能调出来,所以 3 题滚粗。

补题

        lincong 补了 AHL 题。

H-Hamming Distance

        给你两个长度一样的字符串 s、t,构造一个字符串 str,使得 str 与 s 对应位置字母不同的数量,等于 str 与 t 对应位置字母不同的数量(若有多个 str 满足题意,输出字典序最小的那个)。

        一开始 lincong 以为是个简单的贪心,先将目标字符串全部置为 a,然后从后向前替换,结果交上去 WA 了才意识到事情并没有那么简单。

        后来手算了几组数据,发现这种情况没有考虑到:

        aab
        bba

        如果直接贪心的话,会输出 aca,但是显然正确答案是 abc。相当于本来 t 串中的 a 就少,还要扔掉那个 a 换成 c,但是接下来再给一个 b 就可以直接抹平 s 和 t 的差距,同时还能保证字典序最小。

        考虑这种情况的存在,lincong 就想着是不是得从前向后替换,然后发现不知怎样判断一个位置是否应该修改,最后一直调到 VP 结束也没能调出来。

        VP 结束后,lincong 补题时发现,确实是应该正着构造,但是得开一个 sum 数组来记录每个位置向后最多可以调整的数量,这样就可以很好地解决 VP 时遇到的问题。

        具体来说,我们正向构造,对于每一个位置,在保证后面可以调整回来的前提下(通过 sum 数组来判断),优先考虑 a,接着考虑 b,最后考虑其他字母。

        代码如下:

string work(string s,string t)
{
	string q;//构造的字符串q
	int n=s.size();
	sum[n]=0;
	for(int i=n-1;i>=0;i--)
	{
		sum[i]=sum[i+1];
		if(s[i]!=t[i])
			sum[i]++;//计算每个位置最多可以调整的数量
	}
	int cnt=0;//same(q,s)-same(q,t),构造第i位的时候必须保证abs(cnt)<=sum[i+1]
	for(int i=0;i<n;i++)
	{
		if(s[i]==t[i]||abs(cnt)<sum[i+1])//s与t字符相同or后面可以调整回来
		{
			q+='a';
			if(s[i]=='a'&&t[i]!='a')
				cnt++;//与s相同的变多了,并且可以保证abs(cnt)<=sum[i+1]
			if(s[i]!='a'&&t[i]=='a')
				cnt--;//与t相同的变多了,并且可以保证abs(cnt)<=sum[i+1]
			continue;
		}
		char c=min_char(s[i],t[i]);//计算与s[i]和t[i]都不相同的第一个字符
		if(c=='a')
		{
			if(abs(cnt)<=sum[i+1])
				q+='a';//后面可以调整回来,优先考虑a,此时cnt不发生变化
			else if(cnt>0)
				q+=t[i],cnt--;//后面调整不回来了,且现在与s相同的更多,所以放一个和t一样的
			else
				q+=s[i],cnt++;//后面调整不回来了,且现在与t相同的更多,所以放一个和s一样的
		}
		else if(c=='b')
		{
			if((s[i]=='a'&&cnt<sum[i+1])||(t[i]=='a'&&cnt>-sum[i+1]))//后面可以调整回来,优先考虑a
			{
				q+='a';
				if(s[i]=='a')
					cnt++;
				else
					cnt--;
			}
			else if(abs(cnt)<=sum[i+1])
				q+='b';//后面可以调整回来,接着考虑b
			else//后面调整不回来了
			{
				if(s[i]=='a')
					q+=t[i],cnt--;//此时一定是cnt>sum[j+1]
				else
					q+=s[i],cnt++;//此时一定是cnt<-sum[j+1]
			}
		}
		else
		{
			if((s[i]=='a'&&cnt<sum[i+1])||(t[i]=='a'&&cnt>-sum[i+1]))//后面可以调整回来,优先考虑a
			{
				q+='a';
				if(s[i]=='a')
					cnt++;
				else
					cnt--;
			}
			else if((s[i]=='b'&&cnt<sum[i+1])||(t[i]=='b'&&cnt>-sum[i+1]))//后面可以调整回来,接着考虑b
			{
				q+='b';
				if(s[i]=='b')
					cnt++;
				else
					cnt--;
			}
			else
				q+=c;//后面调整不回来了
		}
	}
	return q;
}

L - Two Ants

        给定两个线段,颜色分别为白色和黑色,现在问你在二维平面坐标系中,只能看到白色线段的区域面积是多少?

        计算几何,主要是分类讨论,情况较多,需要注意优先级。

        首先,根据两条线段是否相交,可以分出两种情况:

        一、两条线段相交,此时可分为两种情况:

        1. 交点在端点上,此时 ans 为inf。

        2. 交点不在端点上,此时 ans 为0。

        二、两条线段不相交,此时又可分为两种情况:

        1. 黑色线段与白色线段的延长线相交,此时 ans 为 0。

        2. 黑色线段与白色线段的延长线不相交,此时取两个线段端点之间的连线,根据连线有无交点再分成两种情况:

        (1)有交点。若交点在白色线段一侧,则 ans 为该点与白色线段构成的三角形的面积;若交点在黑色线段一侧,则 ans 为 inf。

        (2)无交点,即连线平行,此时 ans 为 inf。

        特殊地,当白色线段退化成点时,ans 为 0;当黑色线段退化成点时,ans 为 inf。

A - Array Merge

        给你两个序列a、b,将他们合并成一个序列 c,要求是不打乱 a、b 序列原来的相对顺序,且使得 i * c[i] 之和最小。

        若 a 和 b 均为单调不增的序列,那么用类似归并排序的方式,每次取 a 和 b 中的较大值,贪心地合并即可。

        因此,现在的目标是将 a 和 b 转化为单调不增的序列,这里采取的方法是自我合并。

        显然,我们可以将连续的一段进行合并,那么该如何判断一个数应该合并到前面,还是后面呢?

        假设 x、y 已经合并,那么考虑一个独立的数 z,若 z 向前合并,则形成了 xyz;若 z 向后合并,则形成了 zxy,这两种合并方式的差为 x + y - 2 * z。

        那么,我们只需比较 z 和 (x + y) / 2 的大小即可,若 z 较大,那么 z 应该向前合并;否则 z 应该向后合并。

        将这一理论推广到整个序列,我们只需比较相邻两个“块”的均值,若后面的“块”的均值较大,则应该合并到前面的“块”中(一开始“块”的数量为 n,且所有“块”的大小均为 1)。这样合并之后,我们就可以保证 a、b 序列单调不增,且满足 i * c[i] 之和最小。

        具体见代码:

struct node
{
	int sum,siz;
	node(int _sum,int _siz)
	{
		sum=_sum,siz=_siz;
	}
	bool operator>(const node& x)const
	{
		return sum*x.siz>siz*x.sum;
	}
	node operator+(const node& x)const
	{
		return node(sum+x.sum,siz+x.siz);
	}
};
int a[maxn],b[maxn];
node s1[maxn],s2[maxn];
int work()
{
	int n=read(),m=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	for(int i=1;i<=m;i++)
		b[i]=read();
	int tot1=0,tot2=0;
	for(int i=1;i<=n;i++)
	{
		s1[++tot1]=node(a[i],1);
		while(tot1>1&&s1[tot1]>s1[tot1-1])
			s1[tot1-1]=s1[tot1-1]+s1[tot1],tot1--;
	}
	for(int i=1;i<=m;i++)
	{
		s2[++tot2]=node(b[i],1);
		while(tot2>1&&s2[tot2]>s2[tot2-1])
			s2[tot2-1]=s2[tot2-1]+s2[tot2],tot2--;
	}
	int i=1,j=1,k=1,ans=0;
	int pos1=1,pos2=1;
	while(i<=tot1&&j<=tot2)
	{
		if(s1[i]>s2[j])
		{
			int l=pos1,r=pos1+s1[i].siz-1;
			while(l<=r)
				ans+=a[l++]*k,k++;
			i++,pos1=r+1;
		}
		else
		{
			int l=pos2,r=pos2+s2[j].siz-1;
			while(l<=r)
				ans+=b[l++]*k,k++;
			j++,pos2=r+1;
		}
	}
	while(i<=tot1)
	{
		int l=pos1,r=pos1+s1[i].siz-1;
		while(l<=r)
			ans+=a[l++]*k,k++;
		i++,pos1=r+1;
	}
	while(j<=tot2)
	{
		int l=pos2,r=pos2+s2[j].siz-1;
		while(l<=r)
			ans+=b[l++]*k,k++;
		j++,pos2=r+1;
	}
	return ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值