2019-2020 ICPC, Asia Jakarta Regional Contest K. Addition Robot(线段树维护矩阵乘积)

传送门

在这里插入图片描述
给定了只含字符A B的字符串,操作1是区间中的A变成B,B变成A,操作2是区间执行上述操作,输入整数(A,B)输出结果(A,B)

观察发现,A=A+B,B=A+B这种线性方程可以转化成矩阵乘法,(可以类比斐波那契数列的递推公式),而矩阵乘法是有结合律的,可以用线段树维护。

于是试着写成矩阵乘法形式:(我们这里写成右乘的形式,为什么不写成左乘呢?后面会解释。)
[ x , y ] ∗ [ 1 , 0 1 , 1 ] = [ x + y , y ] \left [ x,y \right ] * \begin{bmatrix}1,0\\1,1\end{bmatrix} = \left [x+y,y\right] [x,y][1,01,1]=[x+y,y] (记这个递推矩阵为矩阵1)

[ x , y ] ∗ [ 1 , 1 0 , 1 ] = [ x , x + y ] \left [ x,y \right ] * \begin{bmatrix}1,1\\0,1\end{bmatrix} = \left [x,x+y\right] [x,y][1,10,1]=[x,x+y](记这个递推矩阵为矩阵2)

也就是说,遇到A相当于 [ x , y ] \left [ x,y \right ] [x,y]右乘第一个矩阵,遇到B相当于它右乘第二个矩阵。

操作1相当于把矩阵1变成矩阵2,矩阵2变成矩阵1

我们可以用线段树对每个位置维护两个矩阵乘积,一个是初始的字符串状态下的乘积,一个是进行了操作1情况下的乘积,每次区间修改就是这俩乘积swap一下。

#include<bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define ull unsigned long long
#define ll long long
#define pii pair<int, int>
const int maxn = 1e5 + 10;
const ll mod = 998244353;
const ll inf = (ll)4e17+5;
const int INF = 1e9 + 7;
const double pi = acos(-1.0);
ll inv(ll b){
    
    if(b==1)return 1;return(mod-mod/b)*inv(mod%b)%mod;}
inline ll read()
{
    
    
    ll x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){
    
    if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){
    
    x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
//给定只含AB的字符串,区间修改:A改成B B改成A,
//区间查询:给定x y,L,R,从L往右扫 遇到A:x=x+y 遇到B y=x+y 输出最终的x y 
//A B可以当成两种矩阵 线段树维护矩阵乘积(利用结合律) 翻转操作可以维护两个线段树  
struct mat
{
    
    
	ll a[2][2];
	void initA()//遇到字符A
	{
    
    
		a[0][0]=1,a[0][1]=0;
		a[1][0]=1,a[1][1]=1;
	}
	void initB()//遇到字符B
	{
    
    
		a[0][0]=1,a[0][1]=1;
		a[1][0]=0,a[1][1]=1;
	}
	mat operator * (const mat&f)const 
	{
    
    
		mat ans;
		for(int i=0;i<2;i++)
		{
    
    
			for(int j=0;j<2;j++)
			{
    
    
				ans.a[i][j]=0;
				for(int k=0;k<2;k++)
				{
    
    
					ans.a[i][j]=(ans.a[i][j]+a[i][k]*f.a[k][j]%INF)%INF;
				}
			}
		}
		return ans;
	}
};
struct node 
{
    
    
	#define lc rt<<1
	#define rc rt<<1|1
	mat tr[2];//0是真实的 1是翻转后的乘积
	bool tag;
	int l,r;
}tree[maxn<<2];
char s[maxn];
inline void pushup(int rt)
{
    
    
	tree[rt].tr[0]=tree[lc].tr[0]*tree[rc].tr[0];
	tree[rt].tr[1]=tree[lc].tr[1]*tree[rc].tr[1];
}
inline void build(int rt,int l,int r)
{
    
    
	tree[rt].l=l,tree[rt].r=r;
	if(l==r) 
	{
    
    
		if(s[l]=='A')//0表示真实情况下的乘积  1代表翻转的结果
			tree[rt].tr[0].initA(),tree[rt].tr[1].initB();
		else 
			tree[rt].tr[0].initB(),tree[rt].tr[1].initA();
		return ;
	}
	int mid=l+r>>1;
	build(lc,l,mid);build(rc,mid+1,r);
	pushup(rt);
}
inline void change(int rt) //区间翻转  两个乘积交换
{
    
    
	swap(tree[rt].tr[0],tree[rt].tr[1]);
	tree[rt].tag ^= 1;
}
inline void pushdown(int rt)
{
    
    
	if(tree[rt].tag) 
	{
    
    
		change(lc);
		change(rc);
		tree[rt].tag=0;
	}
}
inline void upd(int rt,int vl,int vr) 
{
    
    
	int l=tree[rt].l,r=tree[rt].r;
	if(r<vl || l>vr) return ;
	if(vl<=l && r<=vr) 
	{
    
    
		change(rt);
		return ;
	}
	pushdown(rt);
	upd(lc,vl,vr);
	upd(rc,vl,vr);
	pushup(rt);
}
inline mat qry(int rt,int vl,int vr)
{
    
    
	int l=tree[rt].l,r=tree[rt].r;
	if(vl<=l && r<=vr) return tree[rt].tr[0];//0代表真实情况下的乘积
	pushdown(rt);
	int mid=l+r>>1;
	if(vr<=mid) return qry(lc,vl,vr);
	else if(vl>mid) return qry(rc,vl,vr);
	return qry(lc,vl,vr)*qry(rc,vl,vr);
}
int n,q;
int main()
{
    
    
	scanf("%d %d",&n,&q);
	scanf("%s",s+1);
	build(1,1,n);
	while(q--)
	{
    
    
		int f=read(),l=read(),r=read();
		ll x,y;
		if(f==1) 
		{
    
    
			upd(1,l,r);
		}
		else 
		{
    
    
			x=read(),y=read();
			mat t=qry(1,l,r);
			ll ans1=t.a[0][0]*x%INF+t.a[1][0]*y%INF;
			ll ans2=x*t.a[0][1]%INF+y*t.a[1][1]%INF;
			printf("%lld %lld\n",ans1%INF,ans2%INF);
		}
	}
	return 0;
}

本题涉及的TRICK:
①线性递推式转化成矩阵乘积
②利用矩阵的结合律,用线段树来维护矩阵乘积 。(这里特别要注意,矩阵乘积没有交换律,就是从左往右算(即:具有从左往右的方向性),而线段树的区间合并也是具有方向性的,也是从左往右,所以这里需要对应起来。回到本题,我们操作2写成矩阵乘法都是在右乘,所以方向和线段树是一致的。)

如果我们把矩阵乘法写成左乘的形式可做吗, 答案是可以的:

[ 1 , 1 0 , 1 ] ∗ [ x y ] = [ x + y y ] \begin{bmatrix}1,1\\0,1\end{bmatrix} * \begin{bmatrix} x\\y\end{bmatrix} =\begin{bmatrix} x+y\\y\end{bmatrix} [1,10,1][xy]=[x+yy]

扫描二维码关注公众号,回复: 13285911 查看本文章

[ 1 , 0 1 , 1 ] ∗ [ x y ] = [ x x + y ] \begin{bmatrix}1,0\\1,1\end{bmatrix} * \begin{bmatrix} x\\y\end{bmatrix} =\begin{bmatrix} x\\x+y\end{bmatrix} [1,01,1][xy]=[xx+y]

遇到A/B就让 [ x y ] \begin{bmatrix} x\\y\end{bmatrix} [xy]左乘相应矩阵,最终也是得到
[ x y ] \begin{bmatrix} x\\y\end{bmatrix} [xy]左边乘了一串矩阵的形式,但是这个矩阵乘式是从左往右计算的,所以第一个字符对应的矩阵应该是在计算的最后一个位置,而线段树的方向也是从左往右的,所以也是线段树维护的区间的最后一个位置。

简洁而言:第i个字符对应矩阵应该放在线段树的n+1-i位置。

代码:

#include<bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define ull unsigned long long
#define ll long long
#define pii pair<int, int>
const int maxn = 1e5 + 10;
const ll mod = 998244353;
const ll inf = (ll)4e17+5;
const int INF = 1e9 + 7;
const double pi = acos(-1.0);
ll inv(ll b){
    
    if(b==1)return 1;return(mod-mod/b)*inv(mod%b)%mod;}
inline ll read()
{
    
    
    ll x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){
    
    if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){
    
    x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
//给定只含AB的字符串,区间修改:A改成B B改成A,
//区间查询:给定x y,L,R,从L往右扫 遇到A:x=x+y 遇到B y=x+y 输出最终的x y 
//A B可以当成两种矩阵 线段树维护矩阵乘积(利用结合律) 翻转操作可以维护两个线段树  
struct mat
{
    
    
	ll a[2][2];
	void initA()//注意这里的矩阵变了,因为改成了左乘
	{
    
    
		a[0][0]=1,a[0][1]=1;
		a[1][0]=0,a[1][1]=1;
	}
	void initB()
	{
    
    
		a[0][0]=1,a[0][1]=0;
		a[1][0]=1,a[1][1]=1;
	}
	mat operator * (const mat&f)const 
	{
    
    
		mat ans;
		for(int i=0;i<2;i++)
		{
    
    
			for(int j=0;j<2;j++)
			{
    
    
				ans.a[i][j]=0;
				for(int k=0;k<2;k++)
				{
    
    
					ans.a[i][j]=(ans.a[i][j]+a[i][k]*f.a[k][j]%INF)%INF;
				}
			}
		}
		return ans;
	}
};
struct node 
{
    
    
	#define lc rt<<1
	#define rc rt<<1|1
	mat tr[2];//0是真实的 1是翻转后的乘积
	bool tag;
	int l,r;
}tree[maxn<<2];
char s[maxn];
int n,q;
inline void pushup(int rt)
{
    
    
	tree[rt].tr[0]=tree[lc].tr[0]*tree[rc].tr[0];
	tree[rt].tr[1]=tree[lc].tr[1]*tree[rc].tr[1];
}
inline void build(int rt,int l,int r)
{
    
    
	tree[rt].l=l,tree[rt].r=r;
	if(l==r) 
	{
    
    
		if(s[n-l+1]=='A')//l对应的字符是n-l+1
			tree[rt].tr[0].initA(),tree[rt].tr[1].initB();
		else 
			tree[rt].tr[0].initB(),tree[rt].tr[1].initA();
		return ;
	}
	int mid=l+r>>1;
	build(lc,l,mid);build(rc,mid+1,r);
	pushup(rt);
}
inline void change(int rt) //区间翻转 代表两个矩阵的交换
{
    
    
	swap(tree[rt].tr[0],tree[rt].tr[1]);
	tree[rt].tag ^= 1;
}
inline void pushdown(int rt)
{
    
    
	if(tree[rt].tag) 
	{
    
    
		change(lc);
		change(rc);
		tree[rt].tag=0;
	}
}
inline void upd(int rt,int vl,int vr) 
{
    
    
	int l=tree[rt].l,r=tree[rt].r;
	if(r<vl || l>vr) return ;
	if(vl<=l && r<=vr) 
	{
    
    
		change(rt);
		return ;
	}
	pushdown(rt);
	upd(lc,vl,vr);
	upd(rc,vl,vr);
	pushup(rt);
}
inline mat qry(int rt,int vl,int vr)
{
    
    
	int l=tree[rt].l,r=tree[rt].r;
	if(vl<=l && r<=vr) return tree[rt].tr[0];
	pushdown(rt);
	int mid=l+r>>1;
	if(vr<=mid) return qry(lc,vl,vr);
	else if(vl>mid) return qry(rc,vl,vr);
	return qry(lc,vl,vr)*qry(rc,vl,vr);
}
inline mat qry1(int rt,int vl,int vr)
{
    
    
	int l=tree[rt].l,r=tree[rt].r;
	if(vl<=l && r<=vr) 
	{
    
    
		return tree[rt].tr[1];
	}
	pushdown(rt);
	int mid=l+r>>1;
	if(vr<=mid) return qry1(lc,vl,vr);
	else if(vl>mid) return qry1(rc,vl,vr);
	return qry1(lc,vl,vr)*qry1(rc,vl,vr);
}
int main()
{
    
    
	scanf("%d %d",&n,&q);
	scanf("%s",s+1);
	build(1,1,n);
	while(q--)
	{
    
    
		int f=read(),l=read(),r=read();
		ll x,y;
		l=n+1-l;//
		r=n+1-r;//同理 区间需要对称一下
		if(l>r) swap(l,r);
		if(f==1) 
		{
    
    
			upd(1,l,r);
		}
		else 
		{
    
    
			x=read(),y=read();
			mat t=qry(1,l,r);
			ll ans1=t.a[0][0]*x%INF+t.a[0][1]*y%INF;
			ll ans2=x*t.a[1][0]%INF+y*t.a[1][1]%INF;
			printf("%lld %lld\n",ans1%INF,ans2%INF);
		}
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_46030630/article/details/120712586