第 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 值之和最小即可。
那么,我们对每个点连接的边按边权排序,取相邻的两条边构成一对即可,时间复杂度 。
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;
}