[省选集训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_l=\max_{i=0}^{l-1}\ nxt_i \]

但是动态维护 \(f\) 十分麻烦,所以我们考虑切换贡献的主体,因为只有单调栈中的元素才可能有贡献,我们设 \(p_i\) 是单调栈的第 \(i\) 个节点,那么答案可以写成:

\[\min\{nxt_{p_{i-1}}-p_i+1\} \]

那么我们维护一个从前往后的极长上升子序列即可,把 \(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\) 必败),那么如果儿子存在必胜点那么他就是必败点,否则这个点是必胜点。这可以写成如下的转移式子:

\[g(x)=1-\prod_{y\in son(x)} g(y) \]

动态维护这个式子可以动态 \(dp\),并且由于要改动 \(a_i\) 我们可以在 \(\tt lct\) 上动态 \(dp\),那么我们先来写出矩阵,设 \(g_l\) 表示轻儿子的 \(g\) 之积:

\[\begin{pmatrix} g&1\\ \end{pmatrix} \times \begin{pmatrix} -g_l&0\\ 1&1 \end{pmatrix} = \begin{pmatrix} g'&1\\ \end{pmatrix} \]

考虑这个矩阵其实只有第一列的两个值有意义,按照套路可以展开:

\[\begin{pmatrix} a_1&0\\ b_1&1 \end{pmatrix} \times \begin{pmatrix} a_2&0\\ b_2&1 \end{pmatrix} = \begin{pmatrix} a_1a_2&0\\ a_2b_1+b_2&1 \end{pmatrix} \]

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

posted @ 2022-02-20 12:06  C202044zxy  阅读(212)  评论(0编辑  收藏  举报