1938.2024 ICPC Asia Pacific Championship - sol

20240302-20240308

2024 ICPC Asia Pacific Championship - Online Mirror

和 Mea,Hanghang 组队一起打,只做了 F,三个人不会 G,我又被简单的 C 搏杀。。。


现阶段没有补完,待更新。进度:11/13

D 和 M 是多项式题目,一道 FFT,一道 NTT,由于笔者太菜不会多项式,所以这两道没有补。

L 是线性基题,笔者不会证明,结论来自于找规律。

部分题目的做法和题解不同,有些是自己想出来的做法,有些则是看不懂题解看选手代码得到的做法。

笔者深知看机翻官方题解的痛苦,所以还是来写一篇题解(英语词汇量不够是这样的)。

或许我写的也像机翻官方题解。


每道题的知识点:

A - 奇妙构造 B - 图论,平面图 C - 二进制相关,找规律 D - 多项式,FFT

E - 简单思维题 F - 思维题,素数相关 G - 鸽巢原理,思维题 H - 签到题,贪心

I - 计算几何(凸包) J - 图论(最短路) K - DS,数论 L - 线性代数

M - 多项式,生成函数,NTT


A. Antiparticle Antiphysics

给你两个仅有字母 A,P 组成的字符串 \(S,E\) 和两个正整数 \(a,p\),请你 构造 一组 合法 操作序列将 \(S\) 变成 \(E\),有以下四种操作:

  • +P i:从左到右第 \(i\) 个字符 P 变成 APA,即在 P 两边各插入一个 A。(如果第 \(i\) 个字符不为 P,则操作不合法)
  • +A i:从左到右第 \(i\) 个字符 A 变成 PAP,即在 A 两边各插入一个 P。(如果第 \(i\) 个字符不为 A,则操作不合法)
  • -P i:从第 \(i\) 个字符起删除连续 \(p\) 个字母 P,需要保证第 \(i \sim i+p-1\) 个字符都是 P,否则操作不合法。
  • -A i:从第 \(i\) 个字符起删除连续 \(a\) 个字母 A,需要保证第 \(i \sim i+a-1\) 个字符都是 A,否则操作不合法。

设你构造出的操作序列的操作个数为 \(k\),你需要保证 \(k \le 35000\),如果无解则输出 -1;反之输出 \(k+1\) 行,第一行为一个正整数 \(k\),接下来 \(k\) 行依次输出 \(k\) 个操作,每个操作形如上面的四种。

\(1 \le |S|,|E| \le 50,S \neq E,5 \le a,p \le 20\)

好题!但是一来就感觉不太可做,或许结合代码看会比较简单。


对于这种构造题,我们首先还是要去手玩样例,去分析一种还原的策略的。

这里我们记 \(A^k\) 表示连续的 \(k\) 个字符 AP 同理。


我们通过手玩可以发现以下简单的规律(A,P 可以互换,设空串为 \(\epsilon\)):

  • \(APA \to AAPAA \to \dots \to A^aPA^a \to P\)
  • \(AA \to APAP \to PP\)
  • \(PA \to PPAP \to AAAP \to APPP\)
  • \(P \to APA \to PAAAA/AAAAP\)
  • \(AAAA \to AAPP \to PPPP\)

而还容易发现一些无解的情况。因为我们每次操作只会 \(+2\),所以如果 \(a\) 是偶数,而 \(S,E\) 中的 A 字符个数差是奇数,那么是无解的;对于 P 同理。


想到这里,似乎有一些感觉了,对于第四条转化,

我们发现我们可以凭空造出 \(4\)A 来放在左右,这是非常好的性质,也为我们后面的推导埋下了伏笔。

而由于我们可以把 \(A^a \to \epsilon\),所以根据裴蜀定理,我们可以知道 \(\epsilon\) 可以和 \(A^{gcd(a,4)}\) 相互转换,仔细分析可以发现,\(4|a,4|p\) 是一种特殊情况啊,我们需要特殊讨论。

除去这种特殊情况之外,其实是简单的,因为总是可以一直用操作 \(1,2\) 使得我们能删掉想删的个数。


在这里插叙一段元操作的代码,\(add\) 对应操作一二,\(sub\) 对应操作三四,或许会好理解一些:

void add(int x){
  int len=strlen(S);
  if(S[x]=='A'){
    ans.pb({"+A",x+1});
    while(len>x) S[len+2]=S[len],--len;
    S[x]=S[x+2]='P',S[x+1]='A';
  }else{
    ans.pb({"+P",x+1});
    while(len>x) S[len+2]=S[len],--len;
    S[x]=S[x+2]='A',S[x+1]='P';
  }
}

void sub(int x){
  int len=strlen(S);
  if(S[x]=='A'){
    ans.pb({"-A",x+1});
    while(x+a<=len) S[x]=S[x+a],++x; 
  }else{
    ans.pb({"-P",x+1});
    while(x+p<=len) S[x]=S[x+p],++x;
  }
}

于是我们考虑这样构造一种方案(以下的想法还仅限于 \(a,p\) 中有一个不是 \(4\) 的倍数,而以下的想法对 \(a\%4\neq 0\) 进行推导,因为 A,P 本质相同,是可以互换的):

我们希望把 \(S\) 串进行修改使得 \(S\) 的前缀变成 \(T\),然后再把某一字符移到结尾,分别删除多余的部分。

S=AP,E=PPAP,a=5,p=4 为例,代码中 \(s1,s2\)\(S\)A,P 的个数,操作次数的计算是粗略的。

  • \(S\) 操作成 \(T\) 的前缀:当前匹配到 \(i\) 位(下标从 \(0\) 开始)。【操作次数:\(O(n)\)

    • 发现 S[i]!=E[i],那么我们考虑对当前这一位 \(i\) 进行一次操作,那么这一位就匹配了,\(AP \to PAPP \to PPAPPP\)
    • 如果 \(S\) 的这一位没有了,例如 S=PP,我们考虑对 \(i-1\) 操作两次,重新回到 \(i-1\) 位进行匹配(这里 \(i=2\)):\(PP \to PAPA \to PPAPPA\)
    for(i=0;i<m;i++) if(S[i]!=E[i]){
      if(S[i]!=0) add(i);
      else add(i-1),add(i-1),--i;
    }
    

    现在任务就变成了把后面的一段消除。

  • 若后面只有一种字符,我们对 \(m\) 位置进行操作,使得两种字符都出现,因为接下来的操作两个字符相互依赖。【操作次数:\(O(1)\)

    \(PPAPPP \to PPAPAPAP\)

    if(!s1||!s2) add(m),(!s1)?s1=2:s2=2;
    
  • 把字符 P 补齐,使得 P 可以被删完,即 P 的个数是 \(p\) 的倍数。(一定会存在合法的方案,之前已经把不合法的方案判了)【操作次数:\(O(p)\)

    \(PPAPAPAP \to PPAPPAPPAP\),接下来的推导省略前缀 PPAP

    while(s2%p) add(i),++i,s2+=2;//i 是第一个 A 的下标
    
  • 将所有的 A 移动到所有 P 后面,且不改变 P 个数:对于每一个 A,我们找到它所在的连续段,对于这个连续段后面的那个 P 操作,使得刚好可以用多次删除操作删完这个 P 前面的 A。【操作次数:\(O(na)\)?】

    \(PAPPAP \to PAAPAPAP \to PAAAAAPAAAAPAP \to PPAAAAPAP \to PPAAAAAPAAP \to PPPAAP \to \dots \to PPPPAAA\)

    for(i=m;S[i];i++) if(S[i]=='A'){
      j=i;
      while(S[j]=='A') ++j;
      if(S[j]==0) break;
      while((j-i)%a) add(j),++j;
      while(j-i>0) sub(i),j-=a;
    }
    
  • 试图补全后面的 A,使得个数能被 \(4\) 整除(从而方便后面的操作):由于 \(a\% 4 \neq 0\),每次我们会多 \(a\)\(A\),所以一定可以完成。【操作次数:\(O(4a)\)

    \(PPPPAAA \to P^3A^5PA^8 \to P^4A^8\)\(i\)A 开始的位置。

    while((j-i)%4){
      for(int k=0;k<a;k++) add(i-1+k);
      sub(i-1),j+=a;
    }
    
  • 用一个 PA 串劈成两段,不改变 P,A 的个数这样就可以删除了:每次可以将 P 右移两个,所以上一步要变成 \(4\) 的倍数。【操作次数:\(O(np)\)?】

    \(PPPPAAAAAAAA \to P^4 P^{p-1}AP^{p-1}AA^6 \to P^3 AP^{p-1}AA^6\to P^3AP^{p-1}PAPA^6 \to P^3AAPA^6 \to \dots \to P^3A^4PA^4\)

    for(int t=0;t<(j-i)/4;t++){
      for(int k=0;k<p-1;k++) add(i+2*t+k);
      sub(i+2*t-1),add(i+2*t+p-1),sub(i+2*t);
    }
    j=i-1+(j-i)/2,--i;//i 是 A 开始的位置,j 是中间 P 的位置
    
  • 通过中间的那个 P 的操作,让两边都有 \(a\) 的倍数个 A,再将 A 全部删除。【操作次数:\(O(a)\)

    \(PPPA^4PA^4 \to PPPA^5PA^5 \to PPPP\),过程中 P 的个数一直不变。

    while((j-i)%a) add(j),++j;
    while(j-i>=a) sub(j+1),sub(i),j-=a;
    
  • 最后把剩下的 \(p\) 的倍数个 P 全部删除即可。【操作次数:\(O(1)\)

    \(PPPP \to \epsilon\)

    while(s2) sub(m),s2-=p;
    

这样我们就完成了一个非常好的构造方式!它可以满足任何非 \(4|a \&\& 4|p\) 的构造!(若 \(4|a\) 就相当于直接把 A,P 交换进行操作)。


现在我们仔细分析一下,其实对于 \(4|a \& \& 4|p\) 的情况出问题的地方就是在于你不保证把 A 变成 \(4\) 的倍数时一定可以完成。

那么现在我们进行手玩第一步的前缀和操作和在 \(a=p=4\) 时的构造(例如 S=AAAAPA,E=APPPPP 是无解的),

可以发现,在构造完前缀之后,若有非 \(4\) 的倍数个 A,P 则无解,而构造过程中(假设第一个要 P)一直不遇见 P ... AA 就会一直构造产生 P

我们发现这个东西和 P ... A 的奇偶性是相关的。(可能这个地方比较感性,欢迎提出更理性的证明)

于是在这种情况下是无解的:

if(!(a%4)&&!(p%4)){
  int cnt=0;
  for(i=0;i<n;i++) for(j=i+1;j<n;j++) cnt+=(S[i]=='P'&&S[j]=='A');
  for(i=0;i<m;i++) for(j=i+1;j<m;j++) cnt+=(E[i]=='P'&&E[j]=='A');
  if((cnt+(s1-e1)/2+(s2-e2)/2)&1) return false;
}

感觉就非常正确,笔者找规律理解的。


发现判断完这些东西,其实我们的特殊情况也可以像之前那样处理了,于是这道题就做完了。

简单加一下操作次数,发现通过是极其轻松的,无论如何都不会过 \(20000\),这里就没有细算了。

代码大概写了 \(3k\),主要部分都在上面提及。代码


B. Attraction Score

给定二维平面上的 \(n\) 个点,第 \(i\) 个点的坐标为 \((x_i,y_i)\)。同时有 \(m\) 条边,第 \(j\) 条边从 \(u_j\) 连到 \(v_j\) 分数为 \(a_j\),在平面图中用 \(u_j \to v_j\) 的线段表示。

保证任意两条边(线段)不相交!!!

现在你需要从里面选出一个若干个点的子集 \(S\) 使得这个子集中的分数最大,一个子集的分数被定义为:

  • 对于任意一对子集中的点对 \((u,v)\) 如果图中存在这样一条 \(u \to v\) 的线段,那么分数会加上这条边的分数 \(a\)
  • 设子集中没有连边的点对 \((u,v)\) 的个数为 \(c\),那么分数减去 \(c^2 \times 10^6\)

你需要找出所有选取方案中,得到分数的最大值,并输出这个分数。

\(1 \le n \le 10^5,1 \le m \le 3 \times 10^5,0 \le a_j \le 10^6\)

图论好题。


首先,我们需要知道这样的一个结论。

由于是在一个平面图上,\(n\) 个点的组成的不相交的边最多有 \(3n-6\) 条。(证明可能是数学内容,由于这是 OI,所以这里不给出证明,笔者也不知道证明)

画图感性理解一下,发现非常正确,并且构造出这个上限是简单的,你每次往一个三角型的里面加一个点,那么它的贡献就是 \(3\)


有了这个结论,我们发现当 \(n \gt 6\) 时,得到的分数一定是负数,所以一定是不优的,不用考虑。

\(n =6\) 时,我们至少会缺少 \(3\) 条边,而一定会存在一个度数最小的点,它的度数最大是 \(4\);我们考虑把这个点删除,至少会减少一条缺失的边,也就是减少 \((3^2-2^2) \times 10^6\),这一定比那 \(4\) 条边的贡献大,所以我们删去它一定是更优的。

所以对于 \(n=6\) 的情况,我们把它转化到 \(n=5\) 一定更优,于是就不需要讨论了。

对于 \(n=5\),用同样的思路,如果它缺失了一条以上的边,我们把那个度数最小的点删去变成 \(n=4\) 一定不劣,所以 \(n=5\) 对答案的贡献只会存在于缺失一条边的情况。

而对于 \(n \lt 5\) 的情况,似乎就非常简单了,可以直接算了。


现在分析清楚了,我们考虑如何去实现呢?

对每一个点枚举?但是如果一个点的度数很大怎么办呢?


这里我们想到从度数最小的点入手,而由于度数之和 \(\le 6n-12\),所以根据 鸽巢原理,度数最小的点的度数一定 \(\le 5\)

我们每次把这个点提出来,对于这个点所连接的点统计答案,再将其删除,变成一个残图,这样就可以不重不漏地统计了。

只需要用一个 priority_queue 来维护。


而注意到我们是允许缺失一条边的,所以当枚举出的一个子集它一条边都不缺失时我们还要考虑是不是还可以加一个点(它有可能和当前枚举到的点不连边),

所以这里我们还需要用 map 维护一下有那些点和一个集合连了边,有一些细节需要特殊处理一下,具体可以看下面这段核心代码:

for(auto i:mp[x]) id[sz]=i.fi,w[sz++]=i.second;//mp 存邻接矩阵
for(int i=0;i<sz;i++) for(int j=0;j<sz;j++) if(mp[id[i]].count(id[j])) f[i][j]=mp[id[i]][id[j]];
for(int i=0;i<(1<<sz);i++){
  int s=0,c=0;
  for(int j=0;j<sz;j++) if(i>>j&1){
    s+=w[j];
    for(int k=j+1;k<sz;k++) if(i>>k&1) f[j][k]==-1?++c:s+=f[j][k];
  }
  s-=M*c*c;ans=max(ans,s);
  if(i&&!c){//没有缺失边,还可以再加入一个点
    vector<int> nw;int sum=0;nw.pb(x);
    for(int j=0;j<sz;j++) if(i>>j&1) nw.pb(id[j]),sum+=w[j];
    sort(nw.begin(),nw.end());//当前已经选的集合
    for(int j=0;j<(int)nw.size();j++){
      vector<int> cur=nw;cur.erase(cur.begin()+j);
      if(C.count(cur)) ans=max(ans,s+C[cur]-M);
    }
    nw.erase(lower_bound(nw.begin(),nw.end(),x));
    C[nw]=sum;//map 记录可选的点,可以不重不漏
  }
}

这样就做完了,时间复杂度 \(\mathcal O(n \log n)\),还有一些常数。代码


C. Bit Counting Sequence

\(p(x)\) 表示 \(x\) 的二进制表达中 \(1\) 的个数,现在给你一个长度为 \(n\) 的数组 \(a_1,a_2, \dots ,a_n\),希望能找到一个 \(x\) 满足 \(a_1,a_2,\dots,a_n\)\(p(x),p(x+1),\dots,p(x+n-1)\) 一一对应,如果存在,则输出最小的 \(x\);反之输出 \(-1\)

\(0 \le a_i \le 60,1 \le n \le 5 \times 10^5\)

本人用了一种纯找规律的方法,并不太用到 \(a_i\) 之差相关的性质。


考虑把 \(p\) 全部列出来,发现是这个样子:\(0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,\dots\)

发现对于每 \(4\) 个数,它一定是形如 \(a,a+1,a+1,a+2\) 的形式;

再把视线放大一点,把每四个看成一个数,取其第一个,发现数组也是 \(0,1,1,2,1,2,2,3,\dots\)

继续放大,每 \(16\) 个数看成一个,发现数列依旧如此!


我们得到什么规律了?

你把 \(4^k\) 看成一个数,那么数列一定是 \(0,1,1,2,1,2,2,3, \dots\)

那么我们只要不断找 \(a,a+1,a+1,a+2\),并且把它改成 \(a\),不断 dfs 下去就可以完成了?

直到我们最后剩下一个数,而找到这个数 \(x\) 的第一次出现位置是简单的,即 \(2^x -1\)

这样我们不断迭代下去,用每一次的 \(4^k\) 去计算它的初始位置就可以了,这样看来是一道非常考验代码能力的题。

场上没有把无解判完——相对失败。但是整体而言代码写起来还是非常有意思的。代码


而队友则给出了另外的思路,简单来说就是根据两个相邻数的差距可以得到一些低位确定,这样似乎会没有那么多细节?代码


E. Duplicates

给你一个 \(n \times n\) 的矩阵,每个位置的值都在 \([1,n]\) 范围内。

一个长度为 \(n\) 的序列是好的,当且仅当存在一个二元组 \((i,j),i\neq j\) 满足 \(a_i =a_j\)

每一次修改可以将矩阵中 \(x\)\(y\) 列的数修改成 \(v\),现在求将矩阵的每一行每一列都变成好的的最小操作次数,并输出这一操作序列。

\(3 \le n \le 100\)

感觉有点诈骗的。


首先,你修改一个点,它的贡献最多是 \(2\),那么我们可以贪心地把这些点都改完,再去改那些贡献是 \(1\) 的点。

而改成的数细想一下也是非常好想的,因为由于一行/一列不是好的,所以 \([1,n]\) 刚好都出现一次,

对于贡献两次的那些点我们把它改成任意一个不是它本身的数都是可以的。


而对于那些贡献为 \(1\) 的修改,直接改成那一好的行/列中任意一个数就可以了,如果一行全部相等,同样还是随便改。

这样就做完了,实现是简单的,主要是限制了值域 \([1,n]\),代码是 \(\mathcal O(n^3)\) 的。代码


F. Forming Groups

\(n\) 个学生,编号为 \(1 \sim n\),每个学生有一个能力值 \(a_i\)。编号为 \(2 \sim n\) 的学生的顺序是不能改变的,而你可以把第 \(1\) 人插入 \(2 \sim n\) 中的任意两个人中间。

现在你可以任意选择一共整数 \(k\)\(k\)\(n\) 的因子,把学生分成 \(k\) 组,具体分组方式类似于 \(\mod k\) 的剩余系:

\(1\) 个在第 \(1\) 组;第 \(2\) 个在第 \(2\) 组;\(\dots\);第\(k+1\) 个在第 \(1\) 组;第 \(k+2\) 个在第 \(2\) 组;\(\dots\);第 \(n\) 个在第 \(k\)组。

每一组的能力值被表示为组内同学的能力值之和,现在你需要找到一个合法的 \(k\) 使得每组能力值的 \(\dfrac{\max}{\min}\)(即 \(\dfrac{能力值最大的组的能力值}{能力值最小的组的能力值}\)) 最小,并输出这个值。

\(2 \le n \le 10^6,1 \le a_i \le 1000\)

场上的唯一贡献,还有一发数组开小了/fn


首先考虑枚举每一个 \(k\),我们希望在 \(O(n \log n)\) 的时间复杂度之内算出答案。

如果单纯地是把每一个 \(1\) 插进去,发现这是做不了的,但为什么要这么想呢?

我们尝试把整个操作看成交换两个数,每次把 \(1\) 和它后面的那个数交换,一直交换到结尾就可以算出所有的情况,

而这种统计是简单的,因为每一次只会改变两组的能力值,所以我们用一个 set 或者 priority_queue 维护一下就可以了,

这样可以做到单次 \(O(n \log n)\)


但是这样交上去会 T,为什么?

容易想到是枚举的因数 \(k\) 太多了,所以我们考虑优化——猜想只有 \(k\) 是素数的时候才最优?

稍微改一下程序自己找一点数据跑一下,发现是显然的,于是在场上你就可以写一发交了,你就可以过了。


可是为什么呢?

发现证明是简单的,我们以 \(k\)\(2k\) 为例。

分成 \(k\) 组相当于先把序列分成了 \(2k\) 组在按照两两联合分成 \(k\) 组,发现这样一定是更优的,因为最坏情况 \(\dfrac{最大+次大}{最小+次小} \gt \dfrac{最大}{最小}\)

用类似的方式我们可以证明其他的分解,所以这样当 \(k\) 是质数时一定是最优的。

那么这道题我们就做完了,最多只会枚举 \(7\) 个质数,所以时间复杂度是 \(\mathcal O(7 \times n \log n)\) 的。代码


G. Personality Test

给你 \(n\) 个长度为 \(m\) 的字符串,每个字符串由 . 和大写英文字母组成,任意两个字符串 \(a,b\) 的相似度被定义为使 \(a_i=b_i,a_i \neq\) .\(i\) 的个数,现在要求你找到一组相似度 至少\(k\) 的字符串 \((x,y),x \lt y\)。如果由多组满足,则输出 \(y\) 最小的;若相同的 \(y\) 有多个 \(x\) 满足条件,则输出 \(x\) 最大的。不存在输出 \(-1\)

\(2 \le n \le 5000,1 \le m \le 3000,1 \le k \le 5\)

三个人都被诈骗哩。


首先,问题的方式就非常奇怪——输出一组?

不让你计数就挺奇怪的。

想一个最简单的暴力,枚举任意两个串,用 \(O(m)\) 的时间比较一下相似度,这样做是 \(\mathcal O(n^2m)\),很显然过不了。

于是考虑优化,搞不好直接往 bitset 想了,于是就回不来了。


可是 \(k\) 很小总是有用意的吧?如果用 bitset 之类去优化就完全没有用到性质了,所以怎么办捏?

发现如果当前枚举到的总的相似度是 \(n^2k\) 了,由于一共只会有 \(O(n^2)\) 对不同的二元组,那么根据 鸽巢原理,一定会有一组的相似度 \(\ge k\) 的!

于是用一个 vector 存一下在第 \(i\) 列为哪个字母的那些字符串,每次依照这些来枚举,这样至多 \(n^2k\) 次就可以找到答案!

这样我们就做完了!想到 鸽巢原理 你就赢了,时间复杂度 \(\mathcal O(kn^2)\)代码


H. Pho Restaurant

给定 \(n\)\(01\) 串,一个操作可以让一个字符从一个字符串移到另一个字符串,现在问最小的操作次数使得每个串要么全是 \(0\) 要么全是 \(1\)

\(1 \le n \le 10^5,\sum |S_i| \le 5\times 10^5\)

用最长的题意描述最简单的问题。


直接贪心就可以了,如果一个串的 \(1\) 个数小就移走 \(1\),反之移走 \(0\)

全一全零的情况可能需要判一下,相信签到题大家都会,时间复杂度线性。代码


I. Symmetric Boundary

给定 \(n\) 个平面直角坐标系上的点 \((x_i,y_i)\),现在需要你在这个平面上面找到一个中心对称的凸多边形,使得这 \(n\) 个点都在这个多边形的顶点或边缘上面。

如果存在输出凸多边形的最小面积;不存在输出 \(-1\)

\(1 \le n \le 30,0 \le x_i,y_i \le 1000\),输入按照逆时针顺序。

ACM 中的 Geo,没有太多思路。


由于我们找到的这个凸多边形是一个中心对称的图形,我们假设它的对称中心是 \((X,Y)\)

那么我们把每一个 \((x_i,y_i)\)\((X,Y)\) 为中心对称过去,得到 \((x_i',y_i')\),也在这个多边形上面。

于是判断就只需要看对称之后得到的 \(2n\) 个点是否构成一个凸包就可以了。


但是非常明显,这样的 \((X,Y)\) 实在太多了,而里面有很多没有用的点,我们如何知道那些点是没有用的呢?

首先考虑任意三个点 \(i,i+1,j\),发现如果中心在它们所构成的三角形中是对这六个点的相对顺序没有影响的。

即对称中心在图中的红色区域:

image

进而我们会发现,我们取这两条红线的中点,并确定一条直线,

对于所有的这 \(n^2\) 条直线,可能的答案一定是在直线的 交点 上面!


为什么呢?(建议自己画图)

首先对于每一个封闭的区域,在里面选点是不会影响图形的相对顺序的,也就是凸包上面的顺序,

而由于每一条直线都是由一些终点所组成的,所以不会存在一种情况使得在里面是凸的在边缘就是凹的了。

且我们的每一条直线所决定的多边形的面积一定是单调的,这是可以简单拆式子证明的,

所以我们选的点一定是在顶点!

(感觉用到两个中点的连线是非常妙的,它不仅可以限制凹凸性和顺序,还可以满足面积的单调)


所以我们只需要把这 \(O(n^2)\) 条直线找出来,两两求交,得到 \(O(n^4)\) 个交点,

再每次用 \(\mathcal O(n \log n)\) 的时间取判断得到的点是否构成凸包就可以了,每次对面积取 \(\min\),代码还是比较好写的。

这样的总时间复杂度是 \(\mathcal O(n^5 \log n)\),可以轻松通过。代码


J. There and Back Again

给定 \(n\) 个点 \(m\) 条边的无向图,边有边权,你需要从 \(1 \to n\) 再走回来,使得两次路径的边的集合不同,并且边权之和最小,求这个最小值。

\(2 \le n \le 10^5,1 \le m \le 3 \times 10^5\)

并不要求不能走重复的边,只要有一条边只在一个路径中走过就行。


看错题然后一眼网络流。

由于只需要一条边不同,所以我们找到最短路之后直接枚举一下那条边不同就可以了。

具体来说,两次 dijsktra 分别以 \(1,n\) 作为起点,找到 \(1 \to n\) 的最短路(这是一定要选的),并且预处理出来每个点到 \(1,n\) 的最短路长度。

于是对于第二次走,枚举每一条边,如果这条边没有在最短路中出现,那么我们就算上 \(u\)\(1\) 的最短路加上 \(v\)\(n\) 的最短路再加上这条边边权即可,

找到最小的方案,直接加上最短路长度输出就可以了。

时间复杂度 \(\mathcal O(n \log n)\)代码(这个程序有 bug,是数据水了?)代码(这个才不会被 Hack!)。


K. Tree Quiz

给定一棵 \(n\) 个点的有根树,存在这样一个长度为 \(n^2\) 的数组 \(a\),它的构造方式是:

for x=1 -> n
    for y=1 -> n
        a[++cnt]=(x-1)*n*n+(LCA(x,y)-1)*n+(y-1)
sort(a+1,a+cnt+1);

注意最后要让 \(a\) 数组从小到大排序。

给出 \(q\) 个询问,每次输入一个整数 \(k\),询问 \(a_k\) 的值。

\(1 \le n,q \le 10^5\)

给出一个单 log 离线做法!!! 下面是第一次写的题解,写着写着发现可以单 log。。。


可能不是题解的做法(没看),给出一个 \(\mathcal O(n \log n + q \log^2n)\) 的离线做法,可能有点常数。


没有强制在线,一来就没往在线去想。

分析式子,由于 \(x,y,LCA(x,y)\) 都是 \([1,n]\) 中的数,所以对于大小的比较就有了优先级,

先让 \(x\) 小,再在 \(x\) 一定的里面找到合适的 LCA 小,最后找 \(y\)

对于 \(x\) 的约束,我们发现是很简单的,\(x=1\) 一定排在 \([1,n]\)\(x=2\) 则在 \([n+1,2n]\),……

那么我们可以用 \(O(1)\) 的时间算出这个排名对应的 \(x\),直接 \(\div n\) 处理一下就可以了,还需要更新一下所找的数在 \(x\) 一定的情况下的排名。

for(int i=1;i<=m;i++) cin>>Q[i].v,Q[i].x=(Q[i].v-1)/n+1,Q[i].v-=1ll*n*(Q[i].x-1),H[Q[i].x].pb(i);

然后我们考虑找 LCA,发现这是可以在一次 dfs 中搞定的。

现在我们依次 dfs 到 \(u\),来枚举 \(x=u\) 的那些询问,而 LCA 为 \(i\) 的个数是可以一路预处理出来的(和子树大小相关),我们可以用树状数组维护,这样方便前缀和。

找排名这件事很适合让二分来做,所以我们直接在 \([1,n]\) 区间内二分即可,每次在树状数组中找到 LCA \(\le mid\) 的个数,稍微比较一下就可以找到 LCA 了。

void dfs2(int u,int fa){
  upd(fa,-sz[u]),upd(u,sz[u]);
  for(auto i:H[u]){
    int l=1,r=n;
    while(l<r) qry(mid)>=Q[i].v?r=mid:l=mid+1;
    Q[i].z=l,Q[i].v-=qry(l-1),F[l].pb({i,son[l]});
  }
  for(auto v:G[u]) son[u]=v,dfs2(v,u);
  upd(fa,sz[u]),upd(u,-sz[u]);
}

找到了 \(x\) 和 LCA,剩下的就是找到 \(y\) 了,发现我们需要实现的就是在以 \(u\) 为根的子树中,找除了 \(v\) 这个儿子的子树,第 \(k\) 大的编号是多少。

笔者尝试用了许多 STL 实现,但发现都不行,于是索性直接用线段树合并完成。

我们同样是二分,每次查个数就可以了,会有一些细节,还需要开两个线段树。

这样的时间复杂度是 \(\mathcal O(n \log n +q \log ^2 n)\) 的。代码


现在来讲那个单 log 做法! 笔者太菜,上面的题解写完了才发现。

不是,我都想到那儿了单 log 不就出了?

聪明的你已经发现,不是把两次二分都变成 线段树二分 就可以完成了,而且会发现其实两次的 dfs 可以合并。

于是我们就得到了离线 \(\mathcal O(n \log n)\) 的做法,但是笔者的常数较大,可能会有更优做法。代码


再在这里提供一个较短的,\(\mathcal O(n \log^3n)\) 的在线做法,队友在场上的做法。代码


L. XOR Operations

给定 \(n\)\(n\) 个数 \(a_1,a_2,\dots,a_n\),你有一个长度为 \(n\) 的数组 \(B=\{ b_1,b_2,\dots,b_n \}\),初始所有值都是 \(0\)

对于一次操作,可以选择两个整数 \(i,j\),进行 \(b_i = b_i \oplus a_i \oplus a_j\)\(b_j = b_j \oplus a_i \oplus a_j\) 的赋值。

现在你可以进行若干次操作,问最后可以得到多少种不同的 \(B\) 数组,方案数 \(\mod 998244353\)

\(2 \le n \le 2 \times 10^5,0 \le a_i \le 2^{30}\)

一篇非常水的题解,三个字——找规律。笔者太菜,看不懂题解。


首先,与异或相关我们就可以往线性基方面想,于是你先写一个暴力出来。

发现答案一定是 \(2^k\) 的形式,然后我们考虑线性无关的性质(瞪应该瞪得出来),接下来加入一些可以被表示出来的数,发现一些规律。

但是发现 \(a_i=0\) 比较麻烦,所以我们为了避免,把 \(a_i\) 变成 \(a_i \times 2+1\) 即可。

这样经过大量的尝试与发现,就可以通过这一道题了!!!代码


Conclusion

  1. 构造题一定要手玩样例,经过大量的模拟之后得到一种可行的操作顺序。(A)
  2. \(n\) 个点的平面图上可构成的不相交的边最多 \(3n-6\) 条,枚举图中点集可以考虑按照度数从小到大枚举。(B)
  3. 每个地方都需要判断一下是否存在不合法的情况啊,找规律的题目一定要仔细!(C)
  4. 与值相关的问题注意值域范围,有些值域可以构成非常简单的做法啊。(E)
  5. 有关计数/贪心的问题(奇怪的数据范围)一定要考虑能否用 鸽巢原理 得到一些结论。(B,G)
  6. 计算几何的很多题目都是取枚举答案,而很多枚举是无用的,所以我们考虑去找到那些有用的点。(I)
  7. 异或相关一般与线性基相关,于是可以找规律。(L)

终,于,写,完,了,工作量有点大的。

posted @ 2024-03-08 20:49  H_W_Y  阅读(218)  评论(0编辑  收藏  举报