GJOI 10.17/10.18 题解
1.AT_arc119_c ARC Wrecker 2
题意

多组测试数据 1≤T≤101\le T\le 101≤T≤10,1≤n≤1051\le n\le 10^51≤n≤105,ai∈[0,109]a_i\in[0,10^9]ai∈[0,109]。
思路
首先容易解决减出负数的情况,直接加一个大数即可。其次对于 a1∼4a_{1\sim 4}a1∼4,先做第一步:
0 a2−a1 a3−a4 00\ a_2-a_1\ a_3-a_4\ 00 a2−a1 a3−a4 0
此时需要 a2−a1=a3−a4a_2-a_1=a_3-a_4a2−a1=a3−a4,才能删完。这个条件等价于 a2+a4=a1+a3a_2+a_4=a_1+a_3a2+a4=a1+a3。
对于 a1∼5a_{1\sim 5}a1∼5 同样如此:
0 a2−a1 a3 a4−a5 00 0 a3−a2+a1 a4−a5 0\begin{matrix} 0\ a_2-a_1\ a_3\ a_4-a_5\ 0 \\ 0\ 0\ a_3-a_2+a_1\ a_4-a_5\ 0 \end{matrix}0 a2−a1 a3 a4−a5 00 0 a3−a2+a1 a4−a5 0
此时需要 a3−a2+a1=a4−a5a_3-a_2+a_1=a_4-a_5a3−a2+a1=a4−a5 才能删完。这个条件等价于 a5+a3+a1=a2+a4a_5+a_3+a_1=a_2+a_4a5+a3+a1=a2+a4。
不难发现一个性质——交错和相等(这和差分有什么关系)。维护奇数位和或者偶数位和,转化为前缀偶数位减去奇数位之和 bib_ibi,转化为子段和为 000 的子段个数,桶维护即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e5+9;
ll id,Q,n,a[N];
ll s[N],ss[2][N],b[N];
unordered_map<ll,ll>cnt;
int main()
{freopen("edit.in","r",stdin);freopen("edit.out","w",stdout);scanf("%lld%lld",&id,&Q);while(Q--){cnt.clear();cnt[0]=1;scanf("%lld",&n);for(int i=1;i<=n;i++){scanf("%lld",&a[i]);s[i]=s[i-1]+a[i];}ss[1][1]=a[1];ss[1][2]=a[1];for(int i=3;i<=n;i+=2){ss[1][i+1]=ss[1][i]=ss[1][i-2]+a[i];}ss[0][1]=0;for(int i=2;i<=n;i+=2){ss[0][i+1]=ss[0][i]=ss[0][i-2]+a[i];}for(int i=1;i<=n;i++){b[i]=s[i]-2*ss[1][i];}ll ans=0;for(int i=1;i<=n;i++){ans+=cnt[b[i]];cnt[b[i]]++;}printf("%lld\n",ans);}return 0;
}
2.P10100 ROIR2023 石头
题意

1≤n,q≤1051\le n,q\le 10^51≤n,q≤105。
思路
注意这是个排列。
先发现几个结论:
- 一个起始点 x0x_0x0 扩展 k−1k-1k−1 次(选数算一次)才能到 ppp,于是 x0∈[p−k+1,p]x_0\in[p-k+1,p]x0∈[p−k+1,p] 或 [p,p+k−1][p,p+k-1][p,p+k−1]。
- 在左右两端区间,合法 x0x_0x0 的分布,必然是连续的。不难想象,这些合法的 x0x_0x0 都是扩展到统一区间,然后用同样步骤扩展到 ppp。
先看 x0∈[p−k+1,p]x_0\in[p-k+1,p]x0∈[p−k+1,p],之所以可以扩展到右边的 ppp 而不去左边的 p−kp-kp−k,是因为 ap−k>apa_{p-k}>a_pap−k>ap。也即 p−k∼x0−1p-k\sim x_0-1p−k∼x0−1 有个最大值 LmxLmxLmx 大于 x0+1∼px_0+1\sim px0+1∼p 的最大值。LmxLmxLmx 使得整个扩展过程不会去到太右边,否则会出现 x0x_0x0 右边有个最大值 >ap−k>a_{p-k}>ap−k,然后被迫扩展到 ap−ka_{p-k}ap−k 的情况。
因为区间越长最大值单调不降,区间越短最大值单调不增,两边都有单调性,可以二分答案。恰好 kkk 步其实不好做,但是可以转化为至多 kkk 步,这样合法 x0x_0x0 的分布就是从 ppp 向前连续的。
用至多 kkk 步减去至多 k−1k-1k−1 步即为答案。区间最大值可以预处理 ST 表。时间复杂度 O(nlogn)O(n\log n)O(nlogn)。
注意边界条件的处理。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e5+9,M=19,inf=3e14;
ll id,n,Q;
ll a[N];
ll lg2[N],fmx[N][M];
void init()
{lg2[1]=0;lg2[2]=1;for(int i=3;i<N;i++)lg2[i]=lg2[i/2]+1;
}
void getST()
{for(int i=1;i<=n;i++)fmx[i][0]=a[i];for(int j=1;j<=lg2[n];j++)for(int i=1;i+(1<<j)-1<=n;i++)fmx[i][j]=max(fmx[i][j-1],fmx[i+(1<<(j-1))][j-1]);
}
ll query_ma(ll l,ll r)
{if(l<1||r>n||l>r)return inf;//边界无穷大,从而无法向外面扩展ll s=lg2[r-l+1];return max(fmx[l][s],fmx[r-(1<<s)+1][s]);
}
ll sol(ll p,ll k)
{if(k<=1)return k;ll l1=max(1ll,p-k+1),r1=p-1,l2=p+1,r2=min(n,p+k-1);ll ans1=p,ans2=p;ll L1=p-k,R2=p+k;while(l1<=r1){ll mid=(l1+r1)>>1;if(query_ma(mid+1,p)<=query_ma(L1,mid-1))ans1=mid,r1=mid-1;else l1=mid+1; }while(l2<=r2){ll mid=(l2+r2)>>1;if(query_ma(p,mid-1)<=query_ma(mid+1,R2))ans2=mid,l2=mid+1;else r2=mid-1;}return ans2-ans1+1;//ans2-p+1 + p-ans1+1 -1
}
int main()
{freopen("rock.in","r",stdin);freopen("rock.out","w",stdout);scanf("%lld%lld%lld",&id,&n,&Q);init();for(int i=1;i<=n;i++)scanf("%lld",&a[i]);a[0]=a[n+1]=inf;getST();while(Q--){ll p,k;scanf("%lld%lld",&p,&k);ll ansk=sol(p,k),ansk1=sol(p,k-1);printf("%lld\n",ansk-ansk1);}return 0;
}
3.CF1781F Bracket Insertion
4.P10880 JRKSJR9 莫队的 1.5 近似构造
接下来是 GJOI 10.18。
1.洛谷 P5329 SNOI2019 字符串
题意

多组测试数据 1≤T≤31\le T\le 31≤T≤3,1≤n≤5×1041\le n\le 5\times 10^41≤n≤5×104。
思路
从前往后扫,如果删去一位,字典序比当前小,那么后面怎么删字典序都没有当前小。所以输出该位。
这些位之外的所有位,删去都会使字典序变大,那么考虑让字典序变化的影响尽量小:即倒着扫回去,把没输出的位输出即可。因为前面的位删了会使字典序变得更大。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=5e5+9;
ll id,Q,n;
char s[N];
ll bel[N],cnt,g[N],tail[N];
bool vis[N];
int main()
{freopen("sort.in","r",stdin);freopen("sort.out","w",stdout);scanf("%lld%lld",&id,&Q);while(Q--){scanf("%lld%s",&n,s+1);ll tot=1;cnt=0;for(int i=1;i<=n;i++){if(s[i]==s[i+1])tot++;else {cnt++;ll num=s[i]-'a'+1;bel[cnt]=num;g[cnt]=tot;tail[cnt]=i;tot=1;}}for(int i=1;i<=cnt;i++){if(s[tail[i]]>s[tail[i]+1]){for(int j=tail[i]-g[i]+1;j<=tail[i];j++)printf("%d ",j);vis[i]=1;}}for(int i=cnt;i>=1;i--){if(vis[i])continue;for(int j=tail[i]-g[i]+1;j<=tail[i];j++)printf("%d ",j);}puts("");for(int i=1;i<=cnt;i++)vis[i]=0;}return 0;
}
2.CF1896E Permutation Sorting
题意

多组测试数据 1≤T≤101\le T\le 101≤T≤10,1≤n≤1051\le n\le 10^51≤n≤105。
思路
注意是 (j mod k)+1(j\bmod k)+1(jmodk)+1。人话题意就是在环上,把还没回家的顺时针转一位,到一个没有已经回家了的人的家。(不理解的可以手玩一下呀)
考虑破环成链。记 iii 人现在的位置 posipos_iposi。若 posi≤ipos_i\le iposi≤i 说明没有跨过 111 直接回家,否则就是跨过了 111。考虑画出当前位置到家的线段:

根据题目的操作,已经回家了的人(比如 555),就把他和他的家删掉。
111 要回家,理论上它要转 7−2=57-2=57−2=5 次,但是它中间有 333 条线段挡路——而这三条线段代表的 4,5,64,5,64,5,6 会比 111 先回家,少了 333 个挡路的 111 回家就只用转 222 次。333 同理,解决掉 444 个挡路的就只用转 6−4=26-4=26−4=2 次。
于是题目转化为,一段线段完全包含多少线段。每个人的答案就是其区间长度(理论上转的次数),减去包含线段数(已经回家被删除的人数)。
这是一个经典的二维数点问题。对于 [li,ri][l_i,r_i][li,ri] 求 li≤lj≤rj≤ril_i\le l_j\le r_j\le r_ili≤lj≤rj≤ri 的 jjj 的个数,可以先对右端点排序,在树状数组依次给每个线段的 lll 上 +1+1+1。扫描线扫描每个右端点,统计 ≥li\ge l_i≥li 的个数即可。时间复杂度 O(nlogn)O(n\log n)O(nlogn)。
代码
#pragma GCC optimise(2)
#pragma GCC optimise(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
inline int read()
{int s=0,w=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();return s*w;
}
inline void write(int x)
{ if(x==0){putchar('0');return;}int len=0,k1=x,c[10005];if(k1<0)k1=-k1,putchar('-');while(k1)c[len++]=k1%10+'0',k1/=10;while(len--)putchar(c[len]);
}
int id,Q,n,a[N];
unordered_map<int,int>pos;
struct term
{int l,r,id;
}p[N<<1];
bool cmp(term x,term y)
{if(x.r!=y.r)return x.r<y.r;return x.l>y.l;
}
int ANS[N];
struct BT
{int T[N<<1];int lowbit(int x){return x&(-x);}void add(int x,int k){for(int i=x;i<=2*n;i+=lowbit(i))T[i]+=k;}int query(int x){int ret=0;for(int i=x;i>=1;i-=lowbit(i))ret+=T[i];return ret;}void clean(){for(int i=1;i<=2*n;i++)T[i]=0;}
}B;
int main()
{freopen("home.in","r",stdin);freopen("home.out","w",stdout);scanf("%d%d",&id,&Q);while(Q--){n=read();for(int i=1;i<=n;i++){a[i]=read();pos[a[i]]=i;}int tot=0;for(int i=1;i<=n;i++){if(pos[i]<=i)p[++tot]=(term){pos[i],i,i},p[++tot]=(term){pos[i]+n,i+n,i};else p[++tot]=(term){pos[i],i+n,i};}sort(p+1,p+tot+1,cmp);for(int i=1;i<=tot;i++){ANS[p[i].id]=p[i].r-p[i].l-(B.query(p[i].r)-B.query(p[i].l-1));B.add(p[i].l,1);}for(int i=1;i<=n;i++)write(ANS[i]),printf(" ");puts("");for(int i=1;i<=tot;i++)B.add(p[i].l,-1);}return 0;
}
反思
要多观察啊!图都画出来了,其实不算难写的。
3.CF1889D Game of Stacks
题意

1≤n≤1051\le n\le 10^51≤n≤105,∑ki≤106\sum k_i\le 10^6∑ki≤106。
思路
~~赛时拼了一个 21pts 的暴力。~~这题思路还是太妙了,最终还是贺了。
考虑把一些重复的步骤给优化掉:这些操作相当于每个栈 iii 给栈顶 ci,topc_{i,top}ci,top 连边。我们把遍历到的点先用一个栈 loop 存下来,因为可能有一大段的元素答案会是同一个,记录答案数组 ansansans:
- 栈 ci,topc_{i,top}ci,top 为空,looplooploop 剩下的元素,答案都是 ci,topc_{i,top}ci,top;
- ci,topc_{i,top}ci,top 已经有了答案 ansci,topans_{c_{i,top}}ansci,top,looplooploop 剩下的元素,答案都是 ansci,topans_{c_{i,top}}ansci,top;
- ci,topc_{i,top}ci,top 曾被遍历过(
loop中有过),即构成了环。我们在loop中删去这个环,并把环上元素对应的栈都进行出栈操作。
这是好理解的,首先这个环的存在或否,对答案没有影响;其次无论从环上哪一个元素开始遍历,都会走过这个环。干脆就把这个无用的环永久删去了。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9;
ll id,n;
stack<ll>stk[N];
ll loop[N],top;
ll ANS[N];
bool vis[N];
ll dfs(ll u)
{if(ANS[u])return ANS[u];if(vis[u]){while(1){ll tem=loop[top--];vis[tem]=0;stk[tem].pop();if(u==tem)break;}}vis[u]=1;loop[++top]=u;if(stk[u].empty())return u;return dfs(stk[u].top());
}
int main()
{freopen("stack.in","r",stdin);freopen("stack.out","w",stdout);scanf("%lld%lld",&id,&n);for(int i=1;i<=n;i++){ll m,x;scanf("%lld",&m);for(int j=1;j<=m;j++){scanf("%lld",&x);stk[i].push(x);}}for(int i=1;i<=n;i++){if(!ANS[i]){top=0;ll t=dfs(i);for(int j=1;j<=top;j++)ANS[loop[j]]=t;}printf("%lld ",ANS[i]);}return 0;
}
