Hetao P5493 [XRCOI Round 2] 好的跃迁 题解 [ 蓝 ] [ 线段树 ]

好的跃迁:蒟蒻的第二道公开赛题。

感觉这题挺显然的,一开始做法是线段树矩阵更是唐氏,交错和做法在最后验题的时候才发现。

#1∼3

暴力模拟即可。

#4∼6

每次修改之后暴力找循环节,然后每次 \(O(1)\) 查询。

#7∼9

观察到只有一个点,并且假设这个点为 \((a,b)\),那么点 \((x,y)\) 经过一次变换后会变为 \((2a-x,2b-y)\)。所以我们直接构造一个 \(3\times 3\) 的矩阵,进行矩阵快速幂即可。

初始矩阵如下:

\[\begin{bmatrix} 1 & x &y\end{bmatrix} \]

递推矩阵如下:

\[\begin{bmatrix} 1 & 2a & 2b \\ 0 & -1 & 0 \\ 0 & 0 & -1\end{bmatrix} \]

#10∼13

只有一次查询,所以我们直接暴力修改,到修改时我们直接一次求解即可。

做法也是矩阵快速幂。因为矩阵乘法没有交换律,所以我们先要把所有点的矩阵全部乘起来,然后再用快速幂处理循环节,循环节之外剩下的一点直接暴力处理掉即可。

#14∼17

没有修改操作,但是起点不固定,所以我们可以采用破环为链的方式,并加上倍增数组来存矩阵,每次查询进行一次 \(O(\log n \times B^3)\) 的倍增跳,算出矩阵即可。

#18∼20

我们考虑把倍增数组换成线段树,在线段树上存矩阵,那么每次修改就是 \(O(B ^3 \log n)\) 的,查询也是 \(O(B ^3 \log n)\)。此处 \(B=3\)。其余细节和前文一致。

注意加上快读快写,以及取模的细节。

这个做法常数较大,无法通过所有测试点。

放一下这个做法的代码:

#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
inline ll llread(){
	ll x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}
void write(int x){
	if(x<0)putchar('-'),x=-x;
	if(x>9)write(x/10);
	putchar(x%10+'0');
}
const int N=200005;
int q,n;
ll mod;
struct mat{
	ll a[4][4];
	mat(){memset(a,0,sizeof(a));}
	mat operator*(const mat&t)const{
		mat res;
		for(int i=1;i<=3;i++)
		{
			for(int k=1;k<=3;k++)
			{
				ll r=a[i][k];
				for(int j=1;j<=3;j++)
				{
					res.a[i][j]=(res.a[i][j]+r*t.a[k][j])%mod;
					if(res.a[i][j]<0)res.a[i][j]+=mod;
				}
			}
		}
		return res;
	}
}f[N];
mat qpow(mat a,ll b)
{
	mat res;
	for(int i=1;i<=3;i++)res.a[i][i]=1;
	while(b)
	{
		if(b&1)res=res*a;
		a=a*a;
		b>>=1;
	}
	return res;
}
struct node{
	int l,r;
	mat v;
};
struct segtree{
	node tr[4*N];
	void build(int p,int ln,int rn)
	{
		tr[p]={ln,rn,f[ln]};
		if(ln==rn)return;
		int mid=(ln+rn)>>1;
		build(lc,ln,mid);
		build(rc,mid+1,rn);
		tr[p].v=tr[lc].v*tr[rc].v;
	}
	void update(int p,int x,mat w)
	{
		if(tr[p].l==x&&tr[p].r==x)
		{
			tr[p].v=w;
			return;
		}
		int mid=(tr[p].l+tr[p].r)>>1;
		if(x<=mid)update(lc,x,w);
		else update(rc,x,w);
		tr[p].v=tr[lc].v*tr[rc].v;
	}
	mat query(int p,int ln,int rn)
	{
		if(ln<=tr[p].l&&tr[p].r<=rn)return tr[p].v;
		int mid=(tr[p].l+tr[p].r)>>1;
		if(rn<=mid)return query(lc,ln,rn);
		if(ln>=mid+1)return query(rc,ln,rn);
		mat tmp;
		tmp=query(lc,ln,rn)*query(rc,ln,rn);
		return tmp;
	}
}seg1;
void construct_dp(mat &m,ll a,ll b)
{
	m.a[1][1]=1;
	m.a[1][2]=2*a%mod;
	m.a[1][3]=2*b%mod;
	m.a[2][2]=-1;
	m.a[3][3]=-1;
}
void construct_s(mat &m,ll a,ll b)
{
	m.a[1][1]=1;
	m.a[1][2]=a;
	m.a[1][3]=b;
}
int main()
{
	q=read(),n=read(),mod=read();
	for(int i=1;i<=n;i++)
	{
		ll a,b;
		a=read();
		b=read();
		construct_dp(f[i],a,b);
		construct_dp(f[i+n],a,b);
	}
	seg1.build(1,1,2*n);
	while(q--)
	{
		int op;
		op=read();
		if(op==1)
		{
			ll i,a,b;
			i=read(),a=read(),b=read();
			mat tmp;
			construct_dp(tmp,a,b);
			seg1.update(1,i,tmp);
			seg1.update(1,i+n,tmp);
		}
		else
		{
			ll a,b,s,k;
			a=read(),b=read(),s=read(),k=llread();
			mat st;
			construct_s(st,a,b);
			mat tmp1=seg1.query(1,s,s+n-1);
			st=st*qpow(tmp1,k/n);
			if(k%n>0)
			{
				mat tmp2=seg1.query(1,s,s+k%n-1);
				st=st*tmp2;
			}
			write(st.a[1][2]);
			putchar(' ');
			write(st.a[1][3]);
			putchar('\n');
		}
	}
	return 0;
}

#21~25

Sol.1

观察到横纵坐标相互独立,所以我们可以开两颗线段树,对横纵坐标分别进行计算。其余部分和之前常数较大的版本一样。此时矩阵被缩小至 \(2 \times 2\),线段树查询和修改复杂度均为 \(O(B^3 \log n)\)。其中 \(B=2\)

对破环成链的部分可以不开两倍空间维护,直接拼接即可做到更小的常数。

#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
inline ll llread(){
	ll x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}
void write(int x){
	if(x<0)putchar('-'),x=-x;
	if(x>9)write(x/10);
	putchar(x%10+'0');
}
const int N=200005;
int q,n,lx;
ll mod;
struct mat{
	ll a[2][2];
	mat(){memset(a,0,sizeof(a));}
	mat operator*(const mat&t)const{
		mat res;
		for(int i=0;i<=1;i++)
		{
			for(int k=0;k<=1;k++)
			{
				ll r=a[i][k];
				for(int j=0;j<=1;j++)
				{
					res.a[i][j]=(res.a[i][j]+r*t.a[k][j])%mod;
					if(res.a[i][j]<0)res.a[i][j]+=mod;
				}
			}
		}
		return res;
	}
}mt1[N],mt2[N];
inline mat qpow(mat a,ll b)
{
	mat res;
	res.a[0][0]=1;
	res.a[1][1]=1;
	while(b)
	{
		if(b&1)res=res*a;
		a=a*a;
		b>>=1;
	}
	return res;
}
struct node{
	int l,r;
	mat v;
};
struct segtree{
	node tr[4*N];
	void build(int p,int ln,int rn)
	{
		if(lx==1)tr[p]={ln,rn,mt1[ln]};
		else tr[p]={ln,rn,mt2[ln]};
		if(ln==rn)return;
		int mid=(ln+rn)>>1;
		build(lc,ln,mid);
		build(rc,mid+1,rn);
		tr[p].v=tr[lc].v*tr[rc].v;
	}
	void update(int p,int x,mat w)
	{
		if(tr[p].l==x&&tr[p].r==x)
		{
			tr[p].v=w;
			return;
		}
		int mid=(tr[p].l+tr[p].r)>>1;
		if(x<=mid)update(lc,x,w);
		else update(rc,x,w);
		tr[p].v=tr[lc].v*tr[rc].v;
	}
	mat query(int p,int ln,int rn)
	{
		if(ln<=tr[p].l&&tr[p].r<=rn)return tr[p].v;
		int mid=(tr[p].l+tr[p].r)>>1;
		if(rn<=mid)return query(lc,ln,rn);
		if(ln>=mid+1)return query(rc,ln,rn);
		mat tmp;
		tmp=query(lc,ln,rn)*query(rc,ln,rn);
		return tmp;
	}
    mat range_query(int x,int y)
    {
        if(x>n)x-=n,y-=n;
        if(y<=n)return query(1,x,y);
        return query(1,x,n)*query(1,1,y-n);
    }
}seg1,seg2;
inline void construct_dp(mat &m,ll a)
{
	m.a[0][0]=1;
	m.a[0][1]=2*a%mod;
	m.a[1][1]=-1;
}
inline void construct_s(mat &m,ll a)
{
	m.a[0][0]=1;
	m.a[0][1]=a;
}
int main()
{
	q=read(),n=read(),mod=read();
	for(int i=1;i<=n;i++)
	{
		ll a,b;
		a=read();
		b=read();
		construct_dp(mt1[i],a);
		construct_dp(mt2[i],b);
	}
	lx=1;
	seg1.build(1,1,n);
	lx=2;
	seg2.build(1,1,n);
	while(q--)
	{
		int op;
		op=read();
		if(op==1)
		{
			ll i,a,b;
			i=read(),a=read(),b=read();
			mat tmp;
			construct_dp(tmp,a);
			seg1.update(1,i,tmp);
			construct_dp(tmp,b);
			seg2.update(1,i,tmp);
		}
		else
		{
			ll a,b,s,k;
			a=read(),b=read(),s=read(),k=llread();
			mat st;
			construct_s(st,a);
			mat tmp1=seg1.range_query(s,s+n-1);
			st=st*qpow(tmp1,k/n);
			if(k%n>0)
			{
				mat tmp2=seg1.range_query(s,s+k%n-1);
				st=st*tmp2;
			}
			write(st.a[0][1]);
			putchar(' ');
			construct_s(st,b);
			tmp1=seg2.range_query(s,s+n-1);
			st=st*qpow(tmp1,k/n);
			if(k%n>0)
			{
				mat tmp2=seg2.range_query(s,s+k%n-1);
				st=st*tmp2;
			}
			write(st.a[0][1]);
			putchar('\n');
		}
	}
	return 0;
}

Sol.2

观察到式子是 \(2a-x\) 的形式,嵌套起来就是 \(2c-(2b-(2a-x))=2c-2b+2a-x\) 的交替和形式。于是我们开两颗线段树维护交替和,分几种情况考虑查询:

  • 最后的 \(i\) 未跨过 \(n\)
    • 直接算交错和即可。
  • 最后的 \(i\) 跨过了一次 \(n\)
    • 跨过 \(n\) 之前交错和和跨过 \(n\) 之后交错和分开算,最后加在一起即可。
  • 最后的 \(i\) 至少跨过两次 \(n\)
    • \(n\) 为奇数时。
      • 因为奇偶性不同,所以大部分全部交错和会被抵消,剩下的一次交错和直接查询即可。再用第二类的方式处理散段即可。
    • \(n\) 为偶数时。
      • 大体和前面一样,在第二类情况上查询全部交错和,乘上系数即可。

时间复杂度 \(O(n\log n)\),比 Sol.1 的做法小几个常数。

代码是容易的,根据上面的模拟就好了,这里就不放了。

posted @ 2025-04-06 18:17  KS_Fszha  阅读(25)  评论(0)    收藏  举报