[省选集训2022] 模拟赛9
货币
题目描述
\(n\) 个国家按照顺序排成一行,有 \(m\) 次事件,第 \(i\) 次事件代表国家 \((u,v)\) 的货币可以流通。
请选择一个连续区间 \([l,r]\),使得按照顺序访问 \([l,r]\) 的国家之后可以搜集所有种类的货币。
\(1\leq n\leq 10^5,1\leq m\leq 2\cdot 10^5\),强制在线。
解法1
设 \(f_l\) 表示以 \(l\) 为左端点的最小右端点,那么可以很容易地用后继来描述,特别地,如果某个点是这种颜色的最后一个点,那么 \(nxt_i=+\infty\),并且 \(nxt_0=\) 每种颜色第一个位置的最大值:
但是动态维护 \(f\) 十分麻烦,所以我们考虑切换贡献的主体,因为只有单调栈中的元素才可能有贡献,我们设 \(p_i\) 是单调栈的第 \(i\) 个节点,那么答案可以写成:
那么我们维护一个从前往后的极长上升子序列即可,把 \(nxt_0\) 传进我们的计算函数中,然后在每个 \(p_i\) 的位置计算贡献即可,对于修改可以启发式合并,那么只会有 \(O(n\log n)\) 次 \(nxt\) 的修改
时间复杂度 \(O(n\log^3n)\),常数和代码量都十分优秀,实现的时候注意到处剪枝。
解法2
本题还有一种复杂度更为正确的做法,考虑每次修改点 \(x\) 的 \(nxt\) 时,影响的只有以 \(x\) 为右端点的 \(f_l\),并且这些 \(f_l\) 一定构成区间。
那么我们可以把这些区间暴力分裂开来,因为 \(f\) 单增的性质,所以分裂之后只会有 \(O(1)\) 个区间合并,这可以把区间个数看成势能,那么启发式合并会增加 \(O(\log n)\) 的势能,每个势能需要用 \(O(\log n)\) 的线段树上二分计算,所以时间复杂度是 \(O(n\log^2n)\) 的,我一开始就想到了这种做法,没想到复杂度可以势能分析。
#include <cstdio>
#include <iostream>
#include <set>
using namespace std;
const int M = 100005;
const int inf = 0x3f3f3f3f;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
void write(int x)
{
if(x>=10) write(x/10);
putchar(x%10+'0');
}
int n,m,k,ans,c[M],nxt[M],tr[M<<2],mx[M<<2];
set<int> s[M];
int ask(int i,int l,int r,int c)
{
if(l==r) return mx[i]>c?c-l+1:inf;
int mid=(l+r)>>1;
return mx[i<<1]>c?min(tr[i],ask(i<<1,l,mid,c))
:ask(i<<1|1,mid+1,r,c);
}
void upd(int i,int l,int r,int id)
{
if(l==r) {mx[i]=nxt[id];return ;}
int mid=(l+r)>>1;
if(mid>=id) upd(i<<1,l,mid,id);
else upd(i<<1|1,mid+1,r,id);
mx[i]=max(mx[i<<1],mx[i<<1|1]);
tr[i]=ask(i<<1|1,mid+1,r,mx[i<<1]);
}
signed main()
{
freopen("currency.in","r",stdin);
freopen("currency.out","w",stdout);
n=read();m=read();k=read();
for(int i=1;i<=n;i++)
{
s[0].insert(i),s[i].insert(i),c[i]=i;
nxt[i]=inf;upd(1,1,n,i);
}
for(int i=1;i<=m;i++)
{
int u=(read()+k*ans-1)%n+1;
int v=(read()+k*ans-1)%n+1;
u=c[u];v=c[v];
if(i==1) ans=n;
if(u==v) {write(ans),puts("");continue;}
if(s[u].size()<s[v].size()) swap(u,v);
s[0].erase(*s[u].begin());
s[0].erase(*s[v].begin());
for(int x:s[v]) s[u].insert(x),c[x]=u;
for(int x:s[v])
{
auto i1=s[u].lower_bound(x),i2=i1;i2++;
if(i1!=s[u].begin())
{
i1--;
if(nxt[*i1]!=x)
nxt[*i1]=x,upd(1,1,n,*i1);
}
if(i2!=s[u].end() && nxt[x]!=*i2)
nxt[x]=*i2,upd(1,1,n,x);
}
s[0].insert(*s[u].begin());
int w=*s[0].rbegin();
ans=ask(1,1,n,w);
write(ans),puts("");
}
}
比赛
题目描述
有 \(n\) 个选手排成一行,编号依次是 \(0,1,2...n-1\),第 \(i\) 个选手的技能属性是 \(a_i\)
初始有一个数字 \(x\),如果选手 \(i\) 发动技能,那么他就会使 \(x\) 变为 \((x+a_i)\bmod n\),最终的 \(x\) 就是胜者的编号。游戏会依次给选手机会发动技能,但是第 \(i\) 个选手会发动技能,当且仅当他发动技能后一定会取得胜利(注意这里的胜利是最终的胜利,而不是暂时性的胜利)
有 \(m\) 次修改,本次修改一个选手的 \(a_i\),每次修改之后都需要求出游戏的胜者。
\(n,m\leq 3\cdot 10^5\)
解法
首先考虑第 \(x\) 个人操作必须要有满足这样条件的 \(y\):\((a_x+y)\bmod=x\),也就是 \(y\) 要成为暂时性的胜者,那么 \(x\) 才有可能发动技能。
我们考虑这样的 \(y\) 的唯一的(如果 \(y\geq x\) 那么认为不存在),可以连边 \((y,x)\) 建出外向森林。然后定义 \(g(i)\) 表示只考虑子树 \(i\),\(i\) 是否能获胜(\(0\) 必胜 \(1\) 必败),那么如果儿子存在必胜点那么他就是必败点,否则这个点是必胜点。这可以写成如下的转移式子:
动态维护这个式子可以动态 \(dp\),并且由于要改动 \(a_i\) 我们可以在 \(\tt lct\) 上动态 \(dp\),那么我们先来写出矩阵,设 \(g_l\) 表示轻儿子的 \(g\) 之积:
考虑这个矩阵其实只有第一列的两个值有意义,按照套路可以展开:
但是还是要注意 \(1,2\) 是从下到上的顺序,当然这都是写代码时的后话了。
还有一点时 \(\tt lct\) 的 \(\tt access\) 怎么实现呢?为了维护 \(g_l\) 我们需要维护虚儿子中 \(0\) 的个数,在虚实切换的时候维护一下就可以了。至于答案只会是 \(0\) 或者 \(0\) 的直接儿子,用一个 \(\tt set\) 维护直接儿子中哪些点必胜然后取最小的即可,时间复杂度 \(O(n\log n)\)
总结
\(\tt ddp\) 是维护复杂信息的有利武器,但是前提是要把决定性的式子写出来,这里不要停留在感性分析上。就像 \(\tt EI\) 所说,要利用好各种意义上的直观:图形直观、数学直观。
#include <cstdio>
#include <iostream>
#include <set>
using namespace std;
const int M = 300005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
void write(int x)
{
if(x>=10) write(x/10);
putchar(x%10+'0');
}
int n,m,a[M],ch[M][2],fa[M],lt[M];set<int> ans;
struct node
{
int k,b;
node(int K=0,int B=0) : k(K) , b(B) {}
node operator & (const node &r) const
//{return node(k*r.k,r.k*b+r.b);}
{return node(k*r.k,k*r.b+b);}
int at(const int &v) {return k*v+b;}
}dp[M];
void up(int x)
{
dp[x]=node(lt[x]?0:-1,1);
if(ch[x][0]) dp[x]=dp[ch[x][0]]&dp[x];
if(ch[x][1]) dp[x]=dp[x]&dp[ch[x][1]];
}
int chk(int x)
{
return ch[fa[x]][1]==x;
}
int nrt(int x)
{
return ch[fa[x]][0]==x || ch[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],z=fa[y],k=chk(x),w=ch[x][k^1];
ch[y][k]=w;fa[w]=y;
if(nrt(y)) ch[z][chk(y)]=x;fa[x]=z;
ch[x][k^1]=y;fa[y]=x;
up(y);up(x);
}
void splay(int x)
{
while(nrt(x))
{
int y=fa[x];
if(nrt(y))
{
if(chk(x)==chk(y)) rotate(y);
else rotate(x);
}
rotate(x);
}
}
int findrt(int x)//splay root meanwhile
{
while(ch[x][0]) x=ch[x][0];
splay(x);return x;
}
void access(int x)
{
int y=0;
for(;x;x=fa[y=x])
{
//remove ch[x][1]
splay(x);
if(ch[x][1] && !dp[ch[x][1]].at(1)) lt[x]++;
//add y as ch[x][1]
if(y && !dp[y].at(1)) lt[x]--;
ch[x][1]=y;up(x);
}
//update the answer
if(findrt(y)==1 && ch[1][1])
{
x=findrt(ch[1][1]);splay(1);
if(dp[x].at(1)) ans.erase(x);
else ans.insert(x);
}
}
void cut(int x,int y)
{
access(y);splay(y);splay(x);
if(!dp[x].at(1)) lt[y]--,up(y);
access(y);//update the answer
if(y==1) ans.erase(x);
splay(x);fa[x]=0;
}
void link(int x,int y)
{
access(y);splay(y);splay(x);
if(!dp[x].at(1)) lt[y]++,up(y);
fa[x]=y;access(x);//update the answer
}
int get()
{
if(ans.empty()) return 0;
return *ans.begin()-1;
}
signed main()
{
freopen("match.in","r",stdin);
freopen("match.out","w",stdout);
n=read();m=read();
for(int i=0;i<n;i++)
{
a[i]=read();
int p=(i-a[i]+n)%n;
if(p<i) link(i+1,p+1);
}
write(get()),puts("");
while(m--)
{
int x=read(),y=read();
int p=(x-a[x]+n)%n;
if(p<x) cut(x+1,p+1);
a[x]=y;p=(x-a[x]+n)%n;
if(p<x) link(x+1,p+1);
write(get()),puts("");
}
}
字符串
题目描述
假设有一个 \(\tt AC\) 自动机,我们给出它 \(\tt trie,fail\) 上的所有边,编号是任意的。
现在只知道这些边,请求出构建该自动机的原串,输入是两棵大小为 \(n\) 的树。
\(n\leq 3\cdot 10^5\)
解法
无根树问题首先考虑定根,定根之后考虑根据 \(\tt fail\) 来写字符的相等限制。也就是对于 \(\tt fail\) 树上的边 \((u,v)\),我们在 \(\tt trie\) 树上一直往上跳,然后对应边相等。这个限制可以用并查集来维护,最后的限制就是一个点的儿子边中没有相等的字符。按道理这里要倍增优化建图,但实际上暴力就可以跑过。
那么问题在于确定根,我们考虑求出两棵树的交集,那么交集上的链就代表着连续相等的字符。如果某个点度数 \(\geq 3\) 那么一定是根(可以证明如果有解那么至多只会有这一个点)
那么现在的情况就是所有点的度数 \(\leq 2\) 了,根一定存在与这一条链上。我们考虑如果某个在链上的点 \(x\) 的邻接点 \(y\),如果 \(fail(y)\) 仍然在链上,那么 \(fail(y)\) 和根的距离 \(\leq 1\)
因为 \(x\) 代表的字符可以写成 aaa
,\(y\) 代表的字符可以写成 aaab
,那么 \(fail(y)\) 可以写成 ab
,考虑一定存在一个点可以使得 \(fail(y)\) 写成 b
,那么就一定和根直接相连了。当然这里还有一些边界情况,可以自己去讨论一下,博主没时间了所以只能口胡这道题。