[省选集训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,那么就一定和根直接相连了。当然这里还有一些边界情况,可以自己去讨论一下,博主没时间了所以只能口胡这道题。

浙公网安备 33010602011771号