G-数据结构-G

\[\huge 近日多做数据结构题,或恐后再读不能醒悟 \]

\[\huge 或记其思路,或骂出题人,或不想刷题,遂有此篇。 \]

\[\]

\[\]

\[\]

\[\]

T1 距离

image

首先这题部分分很多,直接 $ O (n^2) $ 枚举点对,在树上差分即可获得 70 分。

那么正解几乎和部分分就没什么关系了。

首先看到

\[ans_u = \sum_{x ∈ subtree_u} \sum_{y ∈ subtree_u} {\min{ (\vert a_x - a_y \vert , \vert b_x - b_y \vert ) }} \]

中带了一个 $ min $ 的操作,这让我们很不好用数据结构来维护,所以这个时候就得转化题意。

\[a+b = \min { (a,b) } + \max { (a,b) } \]

这个式子是显而易见的,那么通过这个式子我们就可以将 $ \min $ 操作转换为 $ \max $ 操作和简单求和操作。

但是我们转了个dan啊,取 $ min $ 和 取 $ max $ 不是一样难以维护吗。

你说得对,但是,取 $ max $ 操作还可以再次进行转换。

在原本问题中,一个特征值为 $ a_i , b_i $ 的点,可以映射到平面直角坐标系上的一个点 $ (a_i,b_i) $ ,那么两个点 $ (a_i,b_i) $ 和 $ (a_j,b_j) $ 之间的贡献就是 $ \min{(\vert a_i - a_j \vert , \vert b_i - b_j \vert)} $ ,也就是 $ \vert {a_i - a_j} \vert + \vert {b_i - b_j} \vert - \max{(\vert a_i - a_j \vert , \vert b_i - b_j \vert)} $ 。

那么 $ \max{(\vert a_i - a_j \vert , \vert b_i - b_j \vert)} $ 就是 切比雪夫距离

学习切比雪夫距离可看这篇博客

然后人类(反正不是我)发现 切比雪夫距离可以转化为曼哈顿距离 ,也就意味着可以将 取 $ max $ 操作转化为 绝对值求和操作,那么整道题就可以转化为 4个绝对值求和操作 。

怎么转呢?

我们通过图像来看,这是平面直角坐标系上所有与原点的切比雪夫距离为 1 的点:

image

一个边长为 2 的正方形的四条边。

然后我们在看看平面直角坐标系上所有与原点的曼哈顿距离为 1 的点:

image

一个边长为 $ \sqrt 2 $ 的正方形的四条边。

既然都是正方形,那么我给他转一下,然后再将边长乘上 $ \frac{\sqrt{2}}{2} $ 即可。

按理来说矩阵就是用来将一个坐标系旋转的,但是我根本不会好吧。

而且因为这是二维的,我们可以用虚数导一下。

$ x + y i $ 表示点 $ (x,y) $ ,一个虚数 乘上一个 虚数 的含义就是:两者的模长相乘,辐角主值相加。

所以将一个二维点进行旋转可以用虚数来导。那么将 $ (x,y) $ 逆时针旋转 45 °,再乘以 $ \frac{\sqrt{2}}{2} $ ,就相当于 乘上 $ \frac{1}{2} + \frac{1}{2} i $ 。点 $ (x,y) $ 也就变成了 $ (\frac{x-y}{2} , \frac{x+y}{2} ) $ 。

当然你旋转 45°、135°、-45°、-135° 都可以,一般会把 $ (x,y) $ 转化为 $ (\frac{x+y}{2} , \frac{x-y}{2} ) $。

接下来呢,设 $ a1_i = \frac{a_i + b_i}{2} , b1_i = \frac{a_i - b_i}{2} $,整道题都转化为了一个式子:

\[ans_u = \sum_{x ∈ subtree} \sum_{y ∈ subtree} \vert a_i - a_j \vert + \vert b_i - b_j \vert - \vert a1_i - a1_j \vert - \vert b1_i - b1_j \vert \]

也就是维护 四个 绝对值求和。

然后这玩意可以分治计算,具体来说,对于 $ ans[1,n] $ (此处为值域)来说,可以分为两点都在中点的同一侧,和两点在中点的异侧,即:

\[ans[1,n] = ans[1,mid] + ans[mid+1][n] + cnt[1,mid] \times sum[mid+1][n] - cnt[mid+1][n] \times sum[1,mid] \]

直接用线段树维护即可,合并时直接合并,不过需要改写 $ push_up $ 函数。

时间复杂度 $ O (nlogn) $ ,但是常数巨大。

听wkh讲可以学习一下 $ FHQ treap $ 的有交集合并,对于这道题也是 $ O (n log n) $ 的,但是为什么他是最优解啊。

/*
GGrun

*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=5e5+10,inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f,mod=1e9+7;
inline int read(){
	char c=getchar();int x=0;
	while(!isdigit(c))c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x;
}
// #define int ll
int n,cnt,hd[N],rt[N][4],son[(N<<5)][2],ans[N<<5],dfn[N],ys[N],ed[N],fa[N],tot;
int bb[4][N],n1[4];
int sz[(N<<5)];
int sum[(N<<5)];
//rt 0-> a 1-> b 2-> a1 3-> b1
ll a[N],b[N],a1[N],b1[N],pre[N][4];
struct jj{
	int to,next;
}bi[N<<1];
int cntp;
// int q[N<<5],top;
#define mo(x) (x>=mod?x-=mod:0)
inline void add(int x,int y){bi[++cntp]={y,hd[x]},hd[x]=cntp,bi[++cntp]={x,hd[y]},hd[y]=cntp;}
inline void add(int &k,int l,int r,int pos,int v){
	k?k:k=++cnt;
	++sz[k],sum[k]+=v;mo(sum[k]);
	if(l==r)return;
	int mid=l+r>>1;
	pos<=mid?add(son[k][0],l,mid,pos,v):add(son[k][1],mid+1,r,pos,v);
	ans[k]=(((ll)ans[son[k][0]]+ans[son[k][1]]+(ll)sz[son[k][0]]*sum[son[k][1]]-(ll)sz[son[k][1]]*sum[son[k][0]])%mod+mod)%mod;
}
inline int he(int x,int y){
	if(!x||!y)return x^y;
	sz[x]+=sz[y];sum[x]+=sum[y];
	mo(sum[x]);
	son[x][0]=he(son[x][0],son[y][0]);
	son[x][1]=he(son[x][1],son[y][1]);
	ans[x]=(((ll)ans[son[x][0]]+ans[son[x][1]]+(ll)sz[son[x][0]]*sum[son[x][1]]-(ll)sz[son[x][1]]*sum[son[x][0]])%mod+mod)%mod;
	// q[++top]=y;
	// son[y][0]=son[y][1]=sz[y]=sum[y]=ans[y]=0;
	return x;
}
inline void dfs(int x,int f){
	fa[x]=f;dfn[x]=++tot;ys[tot]=x;
	for(int i=hd[x];i;i=bi[i].next){
		int j=bi[i].to;
		if(j!=f)
			dfs(j,x);
	}
	ed[x]=tot;
}
ll anss[N];
int id;
inline void dfs(int x){
	if(id==0)add(rt[x][0],1,n1[0],a[x],bb[0][a[x]]%mod);
	else if(id==1)add(rt[x][1],1,n1[1],b[x],bb[1][b[x]]%mod);
	else if(id==2)add(rt[x][2],1,n1[2],a1[x],bb[2][a1[x]]%mod);
	else add(rt[x][3],1,n1[3],b1[x],(bb[3][b1[x]]%mod+mod)%mod);
	for(int i=hd[x];i;i=bi[i].next){
		int j=bi[i].to;
		if(j!=fa[x]){
			dfs(j);
			rt[x][id]=he(rt[x][id],rt[j][id]);
		}
	}
	(anss[x]+=(id<=1?1:-1)*ans[rt[x][id]])%=mod;
}
signed main(){

	// #ifndef ONLINE_JUDGE
	freopen("distance.in","r",stdin);
	freopen("distance.out","w",stdout);
	// #endif
	// double t=clock();
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	n=read();
	for(int i=1;i<n;++i)
		add(read(),read());
	dfs(1,0);
	for(int i=1;i<=n;++i){
		pre[dfn[i]][0]=a[i]=read(),pre[dfn[i]][1]=b[i]=read(),pre[dfn[i]][2]=a1[i]=(a[i]+b[i]),pre[dfn[i]][3]=b1[i]=(a[i]-b[i]);
		pre[dfn[i]][0]=a[i]=(a[i]<<1),pre[dfn[i]][1]=b[i]=(b[i]<<1);
		mo(pre[dfn[i]][0]);mo(pre[dfn[i]][1]);
		bb[0][i]=a[i],bb[1][i]=b[i],bb[2][i]=a1[i],bb[3][i]=b1[i];
	}
	for(int i=0;i<4;++i){
		sort(bb[i]+1,bb[i]+1+n),n1[i]=unique(bb[i]+1,bb[i]+1+n)-bb[i]-1;
	}
	for(int i=1;i<=n;++i){
		a[i]=lower_bound(bb[0]+1,bb[0]+1+n1[0],a[i])-bb[0];
		b[i]=lower_bound(bb[1]+1,bb[1]+1+n1[1],b[i])-bb[1];
		a1[i]=lower_bound(bb[2]+1,bb[2]+1+n1[2],a1[i])-bb[2];
		b1[i]=lower_bound(bb[3]+1,bb[3]+1+n1[3],b1[i])-bb[3];
	}
	for(int i=1;i<=n;++i){
		pre[i][0]+=pre[i-1][0],pre[i][1]+=pre[i-1][1],pre[i][2]+=pre[i-1][2],pre[i][3]+=pre[i-1][3];
		mo(pre[i][0]);mo(pre[i][1]);mo(pre[i][2]);pre[i][3]=(pre[i][3]%mod+mod)%mod;
	}
	id=0;
	dfs(1);
	for(int i=1;i<=cnt;++i)
		son[i][0]=son[i][1]=sz[i]=sum[i]=ans[i]=0;
	id=1;cnt=0;

	dfs(1);
	for(int i=1;i<=cnt;++i)
		son[i][0]=son[i][1]=sz[i]=sum[i]=ans[i]=0;
	id=2;cnt=0;
	dfs(1);
	for(int i=1;i<=cnt;++i)
		son[i][0]=son[i][1]=sz[i]=sum[i]=ans[i]=0;
	id=3;cnt=0;
	dfs(1);
	// cout<<(double(clock()-t))/1e6<<endl;
	for(int i=1;i<=n;++i)
		cout<<(anss[i]+mod)%mod<<'\n';

}

\[END \ \ \ OF \ \ \ T1 \]

\[\]

\[\]

\[\]

T2、P7671 疯狂动物城

其实就是把一堆板子套在一块了而已。(谁也不知道我调了一上午)

简要题意

1、对于 x 到 y 的路径上的点的权值都加上 v。

2、 给 x 、y ,求 $ \sum_{i ∈ x 到 y 路径上的点} \sum_{j=1}^{dis_{i,y}} a_i \times j $ 。

3、将树恢复到第 x 次 1 操作之后的版本。

简单来说,树剖,区间加,区间求和,主席树。

那么首先树剖非常简单,先把他剖完就行了。然后主要是 线段树怎么维护信息。

这个式子显然可以拆:

\[\sum_{j=1}^{dis_{i,y}} a_i \times j = a_i \times \frac{(1+j)j}{2} \]

我们设 \(k_l,k_r\) 为线段树上编号为k的节点所管的区间, $ sum_k = \sum_{i=k_l}^{k_r} a_i \times \frac{(1+j)j}{2}$ 。
j 是什么?因为每次问的 $ y $ 是不固定的,我们必须得选出一个点来作为 $ y' $ ,然后通过 $ y' $ 的答案转移到 $ y $ 上,那么这个点任意,我们就选用 1 为 $ y' $ 。

怎么转?

首先对于一个询问,他大概长这样:(画的很丑)

image

如果我们直接以 dep 值为到 y 的 距离 $ dis' $ 的话,标出来就是这样:

image

但是对于这个询问,他真实的 $ dis_{i,y} $ 应该是这样的:

image

我们对比两张图,然后考虑怎么去转化答案。

然后你可以发现把 x 到 y 这一段分为 x 到 lca 和 lca 到 y 这两段是比较好转的。

首先对于从 $ x $ 到 $ lca $ 这一段,无论是我们假定的 $ dis_i'$,还是真正的 $ dis_{i,y} $,在这一段上都是一个公差为 1 的 单调递增数列,那么就将每个点的 $ dis' $ 加上 $ i - dep_{lca} $ 即可, $ i $ 此时等于 $ {dep_y} - {dep_{lca}} $ 。即,对于 $ x $ 到 $ lca $ 上的每个点的 $ dis' $ 加一个定值。

对于答案来说,将求和式再次展开合并:

\[\begin{aligned} & ans[x,lca] \\ &= \sum_{i ∈ x 到 lca 上的点} a_i \times \frac{(dis_i + 1)dis_i}{2} \\ &= \sum_{i ∈ x 到 lca 上的点} a_i \times \frac{dis^2_{i}}{2} +a_i \times \frac{dis_i}{2} \\ &= \sum_{i} {a_i \times \frac{dis^{2}_{i}}{2} } + \sum_{i} {a_i \times \frac{dis_i}{2}} \\ &= \sum_{i} {a_i \times \frac{(dis'_{i}+j)^{2}}{2}} + \sum_{i} {a_i \times \frac{dis'_i+j}{2}} \\ &= \sum_{i} {a_i \times \frac{dis'^{2}_{i}}{2} } + \sum_{i} {a_i \times \frac{dis'_i}{2}} + \frac{j + j^2}{2} \times \sum_i{a_i} + j \times \sum_i{a_i} \times {dis'_i} \end{aligned} \]

至此,我们只要在线段树中维护好 $ \sum_i a_i 、 \sum_i {a_i \times dis_i^\prime } 、\sum_{i} {a_i \times \frac{dis'^2_i}{2} } + \sum_{i} {a_i \times \frac{dis_i'}{2}} $ 即可解决 从 x 到 lca 这段路上的 ans。

对于 lca 到 y 这段路上的点,他们的 $ dis' $ 是单调递增的 ,但是 $ dis $ 是单调递减的,不过好在他们的 公差的绝对值都是 1。如果将 $ dis' $ 转为 $ -dis' $ 就和上面的情况相同了。 如果是 $ -dis' $ 的话。

\[ans = \sum_{i} {a_i \times \frac{dis'^{2}_{i}}{2} } - \sum_{i} {a_i \times \frac{dis'_i}{2}} = \sum_{i} {a_i \times \frac{dis'^{2}_{i}}{2} } + \sum_{i} {a_i \times \frac{dis'_i}{2}} - \sum_{i} {a_i \times dis'_i} \]

非常好,用我们已经维护的信息都可以知道。

转化问题说完了,我们来看看线段树怎么维护信息。

首先我们要明确我们得维护哪些信息:

1、$ ans_k = \sum_{i} {a_i \times \frac{dis'^2_{i}}{2} } + \sum_{i} {a_i \times \frac{dis'_i}{2} } $

2、$ suma_k = \sum_i {a_i} $

3、 $ sumsa_k = \sum_i {a_i \times dis'_i} $

为了进行区间加操作,我们还得维护一些别的:

4、 $ sums_k = \sum_i {dis'_i} $

5、 $ sum_k = \sum_{i} \frac {dis'^2_i } {2} + \sum_{i} \frac{dis'_i} {2} $

6、 $ tag_k $

因为是主席树,我们不太好进行 push_down 操作,所以我们采取标记永久化,在询问以及 push_up 的时候加上 $ tag_k $ 的贡献。

然后还有一个防止炸空间的 trick ,因为是树剖,所以一次加操作会被我们分成 不多于 log 次区间加操作,然后我们用的还是主席树,他每进行一次区间加操作都会涨不到 $ log^2 $ 的空间,然而我们用主席树只是为了区分版本,每个版本实际上只用了本版本的 log 次 加操作的最后一次操作之后剩下的哪个版本,有不少版本是被浪费的,所以在写主席树时不一定每到一个点就新建,如果版本一样的话就可以不用新建,就像动态开点那样。




inline void add(int &k,int l,int r,int L,int R,ll v){
	if(tim[k]!=ti){
		ans[++cnt]=ans[k],sum[cnt]=sum[k],sums[cnt]=sums[k],tag[cnt]=tag[k],sumsa[cnt]=sumsa[k],suma[cnt]=suma[k],son[cnt][0]=son[k][0],son[cnt][1]=son[k][1],tim[cnt]=ti,k=cnt;
	}
	if(L<=l&&r<=R)return (void)(ans[k]+=(ll)sum[k]*v%mod,mo(ans[k]),tag[k]+=v,mo(tag[k]),sumsa[k]+=(ll)sums[k]*v%mod,mo(sumsa[k]),suma[k]+=v*(r-l+1)%mod,mo(suma[k]));
	int mid=l+r>>1;
	if(L<=mid)add(son[k][0],l,mid,L,R,v);
	if(R>mid)add(son[k][1],mid+1,r,L,R,v);
	ans[k]=((ll)ans[son[k][0]]+ans[son[k][1]]+(ll)sum[k]*tag[k]%mod)%mod;sumsa[k]=((ll)sumsa[son[k][0]]+sumsa[son[k][1]]+(ll)sums[k]*tag[k]%mod)%mod;suma[k]=((ll)suma[son[k][0]]+suma[son[k][1]]+(ll)tag[k]*(r-l+1)%mod)%mod;
}



#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define mk make_pair
#define ps push_back
#define fi first
#define se second
const int N=1e6+10,inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f,mod=20160501,ny=10080251;
inline ll read(){
	char c=getchar_unlocked();ll x=0,f=1;
	while(!isdigit(c))f=c=='-'?-1:1,c=getchar_unlocked();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar_unlocked();
	return x*f;
}
// #define int ll
#define mo(x) (x>=mod?x-=mod:0)
struct jp{
	int to,next;
}bi[N];
int ti=0;
int ans[N<<4],tag[N<<4],sumsa[N<<4],cnt,suma[N<<4],cntp,laans,sum[N<<4],sums[N<<4];
// sum-> s/2+s*s/2  ans-> ans  tag-> tag of suma  suma-> sum of a_i sums-> sum of s_i sumsa-> sum of s_i*a_i
int son[N<<4][2],sz[N],rt[N],dep[N],hd[N],fa[N],s[N],dfn[N],now,tim[N<<4];
inline void add(int x,int y){bi[++cntp]={y,hd[x]},hd[x]=cntp,bi[++cntp]={x,hd[y]},hd[y]=cntp;}
inline void add(int &k,int l,int r,int L,int R,ll v){
	if(tim[k]!=ti){
		// cout<<l<<' '<<r<<endl;
		ans[++cnt]=ans[k],sum[cnt]=sum[k],sums[cnt]=sums[k],tag[cnt]=tag[k],sumsa[cnt]=sumsa[k],suma[cnt]=suma[k],son[cnt][0]=son[k][0],son[cnt][1]=son[k][1],tim[cnt]=ti,k=cnt;
	}
	if(L<=l&&r<=R)return (void)(ans[k]+=(ll)sum[k]*v%mod,mo(ans[k]),tag[k]+=v,mo(tag[k]),sumsa[k]+=(ll)sums[k]*v%mod,mo(sumsa[k]),suma[k]+=v*(r-l+1)%mod,mo(suma[k]));
	int mid=l+r>>1;
	if(L<=mid)add(son[k][0],l,mid,L,R,v);
	if(R>mid)add(son[k][1],mid+1,r,L,R,v);
	ans[k]=((ll)ans[son[k][0]]+ans[son[k][1]]+(ll)sum[k]*tag[k]%mod)%mod;sumsa[k]=((ll)sumsa[son[k][0]]+sumsa[son[k][1]]+(ll)sums[k]*tag[k]%mod)%mod;suma[k]=((ll)suma[son[k][0]]+suma[son[k][1]]+(ll)tag[k]*(r-l+1)%mod)%mod;
	// cout<<k<<' '<<l<<' '<<r<<' '<<L<<' '<<R<<' '<<suma[k]<<"ADD\n";
}
struct jj{
	ll ans,suma,sumsa;
	inline void operator +=(const jj &x){ans+=x.ans,suma+=x.suma,sumsa+=x.sumsa,mo(ans),mo(suma),mo(sumsa);}
};
inline jj ask(int k,int l,int r,int L,int R,ll ta=0){
	if(L<=l&&r<=R){
		return {((ll)ans[k]+(ll)sum[k]*ta%mod)%mod,((ll)suma[k]+(ll)ta*(r-l+1)%mod)%mod,((ll)sumsa[k]+(ll)sums[k]*ta%mod)%mod};
	}
	int mid=l+r>>1;jj ans={0,0,0};
	if(L<=mid)ans+=ask(son[k][0],l,mid,L,R,(ta+tag[k])%mod);
	if(R>mid)ans+=ask(son[k][1],mid+1,r,L,R,(ta+tag[k])%mod);
	return ans;
}
inline void dfs(int x,int f){
	dep[x]=dep[f]+1;fa[x]=f;sz[x]=1;
	for(int i=hd[x];i;i=bi[i].next){
		int j=bi[i].to;
		if(j!=f){
			dfs(j,x);
			sz[x]+=sz[j];
			if(sz[j]>sz[s[x]])s[x]=j;
		}
	}
}
int top[N],tot,n,m,a[N],ys[N];
inline void dfs2(int x,int zu){
	top[x]=zu;dfn[x]=++tot;ys[tot]=x;
	if(s[x])dfs2(s[x],zu);
	for(int i=hd[x];i;i=bi[i].next){
		int j=bi[i].to;
		if(!dfn[j])dfs2(j,j);
	}
}
inline void jian(int &k,int l,int r){
	k=++cnt;
	if(l==r){
		sum[k]=(((ll)dep[ys[l]]+(ll)dep[ys[l]]*dep[ys[l]])/2ll)%mod,sums[k]=dep[ys[l]],suma[k]=a[l],sumsa[k]=(ll)a[l]*dep[ys[l]]%mod,ans[k]=(ll)sum[k]*a[l]%mod;
		return;
	}
	int mid=l+r>>1;
	jian(son[k][0],l,mid),jian(son[k][1],mid+1,r);
	sum[k]=sum[son[k][0]]+sum[son[k][1]],sums[k]=sums[son[k][0]]+sums[son[k][1]],suma[k]=suma[son[k][0]]+suma[son[k][1]],sumsa[k]=sumsa[son[k][0]]+sumsa[son[k][1]],ans[k]=ans[son[k][0]]+ans[son[k][1]];
	mo(sum[k]),mo(sums[k]),mo(suma[k]),mo(sumsa[k]),mo(ans[k]);
}
inline void add(int x,int y,ll z){
	// int num=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		add(rt[ti],1,n,dfn[top[x]],dfn[x],z);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	add(rt[ti],1,n,dfn[x],dfn[y],z);
}
inline ll ask(int x,int y){
	jj ans1={0,0,0},ans2={0,0,0},zan;
	int dep2=dep[y];
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]){
			zan=ask(rt[now],1,n,dfn[top[y]],dfn[y],0);
			ans2+=zan;
			y=fa[top[y]];
		}
		else{
			ans1+=ask(rt[now],1,n,dfn[top[x]],dfn[x],0);
			x=fa[top[x]];
		}
	}
	if(dep[x]<dep[y])ans2+=ask(rt[now],1,n,dfn[x],dfn[y]);
	else ans1+=ask(rt[now],1,n,dfn[y],dfn[x]);
	int lca=dep[x]<dep[y]?x:y;
	ll ta=dep2-dep[lca]-dep[lca];
	ans1.ans=((ans1.ans+ans1.suma*ta%mod*ta%mod*ny%mod+ans1.suma*ta%mod*ny%mod+ans1.sumsa*ta%mod)%mod+mod)%mod;
	ta=dep2;
	ans2.ans=(ans2.ans-ans2.sumsa+mod)%mod;
	ans2.sumsa=(mod-ans2.sumsa)%mod;
	ans2.ans=(ans2.ans+ans2.suma*ta%mod*ny%mod+ans2.sumsa*ta%mod+ans2.suma*ta%mod*ta%mod*ny%mod)%mod;
	return ((ans1.ans+ans2.ans)%mod+mod)%mod;
}
signed main(){
	#ifndef ONLINE_JUDGE
	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);
	#endif
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	n=read(),m=read();
	for(int i=1;i<n;++i)
		add(read(),read());
	dfs(1,0);dfs2(1,1);
	for(int i=1;i<=n;++i)
		a[dfn[i]]=read();
	jian(rt[ti],1,n);
	now=ti;
	// int na=0;
	for(int i=1,x,y,op;i<=m;++i){
		op=read();
		// laans=0;
		// na =cnt;
		if(op==1)x=read()^laans,y=read()^laans,rt[ti+1]=rt[now],++ti,add(x,y,read()%mod),now=ti;
		else if(op==2){
			x=read()^laans,y=read()^laans,laans=ask(x,y);
			cout<<laans<<'\n';
		}
		else now=read()^laans;
	}
}

\[END \ \ \ OF \ \ \ T2 \]

T3 、 区间 && 比赛(P8868 NOIP2022)

先说一下CSP模拟赛里的一道T4。

区间

image

题意比较清楚,就是对于每一个询问 $ l,r $ ,找出他所有的子区间 $ l_i,r_i $ ,满足 $ l_i ≠ r_i ,A_{l_i} 大于A数组 [l_i+1,r_i-1] 里最大的 , B_{r_i} 大于A数组 [l_i,r_i-1]里最大的 $ 。

考虑把后两个条件分开来看。对于第一个条件,对于同一个左端点 $ l_i $ ,右端点越靠右,区间最大值越大,也就是他有单调性,所以我们可以用单调栈维护。具体来说,维护一个单调严格递减的栈,只有栈里的点才符合第一个要求,那么接下来要去判第二个要求了。假设目前枚举到了点 $ k $ ,因为第一个要求保证了此时栈里的点的 A 值是 $ l_i $ 到 $ k $ 的最大值,那么去和 $ B_k $ 比较时,就直接用栈里的点的 A 值比较即可,也就是说对于栈里的所有点 $ i $ ,只要 $ A_i > B_k $ ,那么这个点就是合法的,而且因为他在单调栈里面,我们可以直接二分查找。所以我们直接把询问离散化下来,按照右端点排序,然后扫过去并记录区间历史和即可。

但是区间历史和怎么记呢?对于每一个右端点,我们可以在总共 $ O (nlogn) $ 的时间复杂度内找到哪些点是合法的,但是这些点并不是连续的,我要把他们的贡献算上去的话每次会被卡到 $ O (n) $ 的,那时间复杂度就被卡成 $ O (n^2) $ 的了。

那么考虑一下为什么这些点不是一段连续的区间呢?因为中间有些点被 $ pop $ 出去了呗。 听起来像是在说废话,但是这就是本题的突破口,因为一个点只会被 $ pop 和 push $ 一次,这也就保证了单调栈的时间复杂度,那么 $ push $ 表示他符合第一个条件, $ pop $ 就表示他已经不符合第一个条件了,而且永远也不会再符合条件了,那么我们可以直接给这些点打上一个标记,表示这个点已经不合法了,然后因为对于第二个条件我们是二分查找的单调栈里的一段区间,贡献就是在单调栈里区间加,也就相当于对于单调栈里合法的最小的下标 $ i $ ,和当前点 $ k $ ,所形成的区间 $ [i,k] $ 上直接区间加,然后删去其中不合法的点的贡献即可,那么这个删去不合法的贡献的过程可以通过刚才打的标记(不是懒惰标记,别搞混了)直接在线段树区间加的过程中实现。具体来说,对于一段区间 $ [l,r] $ ,对他进行一次区间加,原本贡献应是 $ r-l+1 $ ,而实际贡献应是 $ r-l+1-tag_k $ ,$ tag_k $ 表示 $ [l,r] $ 这段区间被标记的点的个数。

至此,本道题可以 $ O(nlog(n)) $ 的时间复杂度内解决。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define int ll
typedef pair<int,int> pii;
#define fi first
#define se second
#define mk make_pair
#define ps push_back
const int N=1e6+10,inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f,mod=1e9+7;
inline ll read(){
	char c=getchar();ll x=0,f=1;
	while(!isdigit(c))f=c=='-'?0:-1,c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return f?x:-x;
}
int n,Q,a[N],b[N];
struct jj{
	int l,r,id;
}q[N];
ll sum[N<<2],tag[N<<2],c[N<<2],ta[N<<2];
inline void down(int k,int len1,int len2){
	if(ta[k])sum[k<<1]+=ta[k]*(len1-tag[k<<1]),sum[k<<1|1]+=ta[k]*(len2-tag[k<<1|1]),ta[k<<1]+=ta[k],ta[k<<1|1]+=ta[k],ta[k]=0;
}
inline void add(int k,int l,int r,int pos){
	if(l==r)return (void)(++tag[k]);
	int mid=l+r>>1;
	down(k,mid-l+1,r-mid);
	pos<=mid?add(k<<1,l,mid,pos):add(k<<1|1,mid+1,r,pos);
	tag[k]=tag[k<<1]+tag[k<<1|1];
}
inline void add(int k,int l,int r,int L,int R){
	if(L<=l&&r<=R)return (void)(sum[k]+=r-l+1-tag[k],ta[k]++);
	int mid=l+r>>1;
	down(k,mid-l+1,r-mid);
	if(L<=mid)add(k<<1,l,mid,L,R);
	if(R>mid)add(k<<1|1,mid+1,r,L,R);
	sum[k]=sum[k<<1]+sum[k<<1|1];
}
inline int ask(int k,int l,int r,int L,int R){
	if(L<=l&&r<=R)return sum[k];
	int mid=l+r>>1,ans=0;
	down(k,mid-l+1,r-mid);
	if(L<=mid)ans=ask(k<<1,l,mid,L,R);
	if(R>mid)ans+=ask(k<<1|1,mid+1,r,L,R);
	return ans;
}
int top;
pii st[N];
ll ans[N];
signed main(){

	// #ifndef ONLINE_JUDGE
	freopen("interval.in","r",stdin);
	freopen("interval.out","w",stdout);
	// #endif

	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	n=read();
	for(int i=1;i<=n;++i)
		a[i]=read();
	for(int i=2;i<=n;++i)
		b[i]=read();
	Q=read();
	for(int i=1;i<=Q;++i){
		q[i].l=read(),q[i].r=read();q[i].id=i;
	}
	sort(q+1,q+1+Q,[](const jj&x,const jj&y){return x.r<y.r;});
	for(int i=1,j=1;i<=n;++i){
		int l=1,r=top,pos=0;
		while(l<=r){
			int mid=l+r>>1;
			if(st[mid].se<b[i])pos=mid,r=mid-1;
			else l=mid+1;
		}
		// cout<<i<<' '<<pos<<' '<<st[pos].fi<<endl;
		if(pos&&st[pos].fi<i)add(1,1,n,st[pos].fi,i-1);
		while(j<=Q&&q[j].r==i){
			ans[q[j].id]=ask(1,1,n,q[j].l,i),++j;
		}
		while(top&&st[top].se<=a[i])add(1,1,n,st[top].fi),--top;
		st[++top]={i,a[i]};
	}

	for(int i=1;i<=Q;++i)
		cout<<ans[i]<<'\n';
	return 0;
}

\[\]

好的,刚才我们提到了 区间历史和 ,如果你学过的话,那么上面这道题就是一个板子。接下来再介绍一道区间历史和的例题吧,其实和这道题很像,只不过线段树难写了点。

比赛

题意仍然是给一堆询问 $ [l,r] $ ,问他的所有子区间 $ [l_i,r_i] $ ,求 $ \max_{k ∈ [l_i,r_i]}{A_k} \times \max_{k ∈ [l_i,r_i]}{B_k} $ 。

又是对于一堆区间他的子区间的所有贡献的题,我们自然可以离线下来然后扫描线扫过去。接下来想想怎么维护 $ max $ 操作。仍然是单调栈,像取 $ max ,min $ 这种有单调性的操作直接上单调栈,进行区间覆盖操作。具体来讲,维护一个严不严格都行的单调递减栈,那么每次 $ pop $ 完后栈顶元素 + 1 到 目前枚举点 $ i $ 这段区间的 A/B 的最大值都应是 $ A_i / B_i $ ,然后直接进行区间赋值。每次枚举完一个右端点后,就对整个区间的历史版本加一,说人话就是让他们的答案都加上目前自己的 $ A_j \times B_j $ 。

那么直接就来到了线段树上,所以接下来讲讲怎么维护这颗线段树。

当前扫描线扫到了右端点 $ r $ ,对于树上一个节点 $ k $ ,他管的区间是 $ l_k , r_k $ ,首先我们需要历史答案和 $ ans_k $ ,为了维护 $ ans_k $ ,我们得知道目前的 $ \sum_{j=l_k}^{r_k}{ \max_{p=j}^{r}A_p \times \max_{p=j}^{r}B_p } $ (以下简称 $ \sum{AB} $ ),那么为了维护 $ \sum{AB} $ ,我们还得维护一些东西,因为我们每次是分开去给A和B赋值,所以当给 A 赋值时 $ \sum{AB} = A \times \sum{B} $ ,所以我们还需要维护 $ \sum{B} $ ,同理,我们还需要维护 $ \sum{A} $ 。

此时我们已经维护了 $ \sum{AB} 、 \sum{A} 、 \sum{B} 、 ans $ 这些已经达到了信息闭环了(信息闭环就是说我已经可以用已知的信息维护已知的信息了,是线段树维护信息是否可行的重要标志)。但因为是区间加,我们还需要维护 懒惰标记。首先需要 $ tag_A 、 tag_B 、 tag_C $ 分别表示 A区间赋值、B区间赋值 和 答案求和次数的懒标。接下来考虑怎么维护他们。

一眼看上去是不是很好维护,直接就在add函数里修改就行。但其实你交上去就会发现WA了。为什么?你百思不得其解,对着大样例调了一组又一组,然后发现这些 $ tag $ 之间会产生时间上的冲突,举个例子:一先对一段区间 $ [l,r] $ 进行了 A 赋值操作,然后对他进行一次求和,然后又对他进行了一次 A 赋值操作,然后在进行一次求和操作,然后询问他的一个真子区间,此时你需要进行 push_down 操作,在 push_down 里面,你需要同时将三个 $ tag $ 下传下去,先下传哪个就有了争议,似乎先下传哪个都不对,就像这个例子,因为这两个求和操作所加的 $ \sum{AB} $ 的值并不相同,无论谁先下传都不对。 如果你在这里疑惑了,建议先做一下 P3373 【模板】线段树 2】 这道题比较简单,但它里面处理了 $ tag乘 $ 和 $ tag加 $ 的冲突,很有益于启发这道题的 $ tag $ 的冲突解决。 好,类比这道题的处理方式,我们去解决这个冲突。冲突在于 $ tag_A 、 tag_B $ 与 $ tag_C $ 无法维护谁先谁后,那么我们尝试通过钦定一个在前并改写他的方式解决冲突。显然赋值操作无法被改写,所以我们只能尝试去改写 $ tag_C $ 。

首先明确 $ tag_C $ 表示的是 求和次数的懒标 , 或者说 是 $ \sum{AB} $ 的求和次数的懒标,那么对于有 $ tag_A 、 tag_B $ 的情况,我们相当于已知当前 $ \sum{AB} $ 了,直接把 $ \sum{AB} $ 的懒标当成 常数的懒标即可,此处用 $ tag_c $ 表示,然后对于只有 $ tag_A $ 的情况,我们可以将 $ \sum{AB} $ 的懒标当成 $ \sum{B} $ 的懒标,同理对于只有 $ tag_B $ 的情况,将 $ \sum{AB} $ 的懒标当成 $ \sum{A} $ 的懒标,分别用 $ tag_a , tag_b $ 表示,同时将上面用到的 $ tag_C $ 更改为 $ tag_{ab} $,那么我们现在已经维护了 $ tag_{ab} , tag_{a} , tag_{b} , tag_c , tag_A , tag_B $ 六种懒标,接下来看看他们有没有达到信息闭环的要求呢? 当标记下传时,我们钦定了先下传 $ tag_{ab} $ 这样的求和标记,那么他们可以通过是否有 $ tag_A 、 tag_B $ 在四个求和标记里面转化,最后转到 $ tag_c $ 里就不会再转化了,只需要乘上当前区间的 $ len $ 就是他的贡献了,所以对于这六个已经达到信息闭环了,所以至此我们的所有信息都已经达到了信息闭环,可以直接上线段树了。

至此本题还没有结束。

这题的线段树写起来感觉有点狗屎,在此推荐另外一种写法。可看博客
数据结构闲谈:范围分治的「双半群」模型

我对于他的理解就是,可以把所有修改操作分为三件事 其中两件 标记对标记的作用标记对信息的作用 对应着 push_down 和 add 函数中对信息的直接修改,还有一件 信息与信息的合并 对应着 push_up 。其实就相当于普通线段树中写的 add_tag 函数。这样写起来会方便很多。

要学习线段树的话,可以去看看 线段树进阶 Part 1 ,这篇写的非常好,看着也舒服。

如果对于本题你真的还不会怎么合并信息、标记的话
// 标记作用于标记 jiaa -> tagA jiab -> tagB
inline void operator +=(const tag&x){
    if(jiaa&&jiab)tagc+=x.tagab*jiaa*jiab+x.taga*jiaa+x.tagb*jiab+x.tagc;
    else if(jiaa)tagc+=x.taga*jiaa+x.tagc,tagb+=x.tagb+x.tagab*jiaa;
    else if(jiab)tagc+=x.tagb*jiab+x.tagc,taga+=x.taga+x.tagab*jiab;
    else tagab+=x.tagab,taga+=x.taga,tagb+=x.tagb,tagc+=x.tagc;
    if(x.jiaa)jiaa=x.jiaa;
    if(x.jiab)jiab=x.jiab;
}

// 标记作用于信息 ab -> sumab a-> suma b-> sumb 

inline void operator +=(const tag&x){
    ans+=x.tagab*ab+x.taga*a+x.tagb*b+x.tagc*len;
    if(x.jiaa&&x.jiab)ab=x.jiaa*x.jiab*len,a=x.jiaa*len,b=x.jiab*len;
    else if(x.jiaa)ab=x.jiaa*b,a=x.jiaa*len;
    else if(x.jiab)ab=x.jiab*a,b=x.jiab*len;
}

// 信息作用于信息 

inline void operator +=(const jj&x){
    ans+=x.ans,ab+=x.ab,a+=x.a,b+=x.b,len+=x.len;
}


#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define mk make_pair
#define ps push_back
const int N=1e6+10,inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f,mod=1e9+7;
inline int read(){
	char c=getchar_unlocked();int x=0;
	while(!isdigit(c))c=getchar_unlocked();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar_unlocked();
	return x;
}
int n;
ull a[N],b[N];
struct tag{
	ull tagab,taga,tagb,tagc,jiaa,jiab;
	inline void operator +=(const tag&x){
		if(jiaa&&jiab)tagc+=x.tagab*jiaa*jiab+x.taga*jiaa+x.tagb*jiab+x.tagc;
		else if(jiaa)tagc+=x.taga*jiaa+x.tagc,tagb+=x.tagb+x.tagab*jiaa;
		else if(jiab)tagc+=x.tagb*jiab+x.tagc,taga+=x.taga+x.tagab*jiab;
		else tagab+=x.tagab,taga+=x.taga,tagb+=x.tagb,tagc+=x.tagc;
		if(x.jiaa)jiaa=x.jiaa;
		if(x.jiab)jiab=x.jiab;
	}
}ta[N];
struct jj{
	ull ans,ab,a,b,len;
	inline void operator +=(const tag&x){
		ans+=x.tagab*ab+x.taga*a+x.tagb*b+x.tagc*len;
		if(x.jiaa&&x.jiab)ab=x.jiaa*x.jiab*len,a=x.jiaa*len,b=x.jiab*len;
		else if(x.jiaa)ab=x.jiaa*b,a=x.jiaa*len;
		else if(x.jiab)ab=x.jiab*a,b=x.jiab*len;
	}
	inline void operator +=(const jj&x){
		ans+=x.ans,ab+=x.ab,a+=x.a,b+=x.b,len+=x.len;
	}
}tr[N];
inline void down(int k){
	if(!ta[k].tagab&&!ta[k].taga&&!ta[k].tagb&&!ta[k].tagc&&!ta[k].jiaa&&!ta[k].jiab)return;
	tr[k<<1]+=ta[k],tr[k<<1|1]+=ta[k],ta[k<<1]+=ta[k],ta[k<<1|1]+=ta[k];ta[k]={0,0,0,0,0,0};
}
inline void add(int k,int l,int r,int L,int R,ull v,int op){
	if(L<=l&&r<=R){
		tag ji;
		if(!op)ji={1,0,0,0,0,0};
		else ji={0,0,0,0,(op==1?v:0),(op==2?v:0)};
		tr[k]+=ji,ta[k]+=ji;
		// cout<<k<<' '<<l<<' '<<r<<' '<<L<<' '<<R<<' '<<v<<' '<<op<<"    "<<tr[k].ans<<' '<<tr[k].a<<' '<<tr[k].b<<endl;
		return;
	}
	down(k);
	int mid=l+r>>1;
	if(L<=mid)add(k<<1,l,mid,L,R,v,op);
	if(R>mid)add(k<<1|1,mid+1,r,L,R,v,op);
	tr[k]=tr[k<<1];tr[k]+=tr[k<<1|1];
}
inline ull ask(int k,int l,int r,int L,int R){
	if(L<=l&&r<=R)return tr[k].ans;
	down(k);
	int mid=l+r>>1;ull ans=0;
	if(L<=mid)ans+=ask(k<<1,l,mid,L,R);
	if(R>mid)ans+=ask(k<<1|1,mid+1,r,L,R);
	return ans;
}
inline void jian(int k,int l,int r){
	tr[k].len=r-l+1;
	if(l==r)return;
	int mid=l+r>>1;
	jian(k<<1,l,mid),jian(k<<1|1,mid+1,r);
}
struct ak{
	int l,r,id;
}q[N];
int q1[N],top1,q2[N],top2;
ull ans[N];
signed main(){

	#ifndef ONLINE_JUDGE
	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);
	#endif

	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	n=read(),n=read();
	for(int i=1;i<=n;++i)
		a[i]=read();
	for(int i=1;i<=n;++i)
		b[i]=read();
	jian(1,1,n);
	int Q=read();
	for(int i=1;i<=Q;++i){
		q[i].l=read(),q[i].r=read(),q[i].id=i;
	}
	sort(q+1,q+1+Q,[](const ak&x,const ak&y){return x.r<y.r;});
	a[0]=b[0]=1e14;
	for(int i=1,j=1;i<=n;++i){
		while(a[q1[top1]]<=a[i])--top1;
		while(b[q2[top2]]<=b[i])--top2;
		add(1,1,n,q1[top1]+1,i,a[i],1);
		add(1,1,n,q2[top2]+1,i,b[i],2);
		add(1,1,n,1,i,0,0);
		// for(int j=1;j<=(n<<1);++j)
		// 	down(j);
		// cout<<q1[top1]+1<<' '<<i<<' '<<a[i]<<" ji111111111\n";
		// cout<<q2[top2]+1<<' '<<i<<' '<<b[i]<<" ji222222222\n";
		while(j<=Q&&q[j].r==i)
			ans[q[j].id]=ask(1,1,n,q[j].l,i),++j;
		// if(i==2)return 0;
		q1[++top1]=i,q2[++top2]=i;
	}
	for(int i=1;i<=Q;++i)
		cout<<ans[i]<<'\n';


	return 0;
}

\[END \ \ \ \ OF \ \ \ \ T3 \]

T4 、 倒水(多校A层冲刺NOIP2024模拟赛17 T4)

非常吊的一道题。

首先手膜一下倒水的过程:

一开始肯定是从第一杯里往后面的杯子 $ x $ 倒水:

image

然后中间那几个没水的杯子不用管,接下来来到了你刚到水的杯子 $ x $ ,然后他去倒水:

假设他还是往后倒,倒给了杯子 $ y $ :

image

这个过程中和 杯子 1 给 杯子 $ x $ 倒水是一样的。

如果他往前倒:

image

那么可以发现后面的杯子一定都没有水了,所以过程就此结束。

总结上面的过程:

从杯子 1 开始向右倒水,那么两个杯子之间不用管,下次倒水一定发生在刚才被倒水的那个杯子中,然后分为继续向右倒和向左倒两种情况。继续向右倒的话,就和一开始的过程一样,向左倒的话倒水过程就此结束。

\[结论:整个倒水过程都是从杯子 1 开始一直向右倒,直到在某个杯子时向左倒,过程结束 \]

画成图就是:

image

$ f_{i,k} $ 表示第 $ i $ 个杯子目前有 $ k $ 的水的概率, $ a_i $ 表示杯子 $ i $ 的容量。

从前往后倒的时候好说,从 $ f_{i,k} $ 转移到了 $ f_{ j , min(k,a_j) } $ 。但是向前转移的时候好像还得考虑前面的杯子中有多少水才行,这就成 $ O (n^4) $ 的了。但是这道题很特殊,整个倒水过程只会有一次向前倒水的过程,我们想一下这次到谁会到给谁: 如果倒给了一个空杯子,那么也就是倒出去了 $ min(k,a_j) $ 的水。如果是一个已经有过水的杯子,因为整个倒水过程很单一,我目前这个杯子 $ i $ 里面的水一定是从杯子 $ j $ 里倒出来的,所以我往回倒的时候一定会把水全倒回去,也就是倒 $ k $ ,因为这个时候 $ k $ 一定 小于等于 $ a_j $ ,所以无论前面的杯子有没有倒过水我都是 倒回去了 $ min(k,a_j) $ 的水。

这样的话就可以 $ O(n^3) $ 去转移了。最后答案就是 $ \sum_{k=1}^{a_i} f_{i,k} \times k $ 。

一个细节是:每次转移都需要乘上单次选择数的逆元,即 $ (n-1)^{mod-2} $ 。

但是其实我们不用最后去用 $ f $ 数组去统计答案,我们想一下最终杯子里的水都是从哪来的:

一个杯子中被倒进来水之后一定还会倒出去一些,那么如果他能剩下一些水的话,这些剩下的水一定这个杯子的答案有贡献,那些被到出去的水也一定对被倒水的那个杯子的答案有贡献。

所以有贡献的水只有两种情况:

1、给别的杯子倒水时剩下的

2、被右边的杯子倒回来的

以上这两种转移可以直接加入答案中而不用再加入 $ f $ 中。

那么整个过程又可以再简化,我们已经不用往回转移了,那么我们稍微改一下 $ f $ 的定义:从左边转移过来的水的概率,整个过程分为了:

1、从左边向右边转移水:

转移到右边的 f 上
给自己留下的水直接计入自己的答案

2、从右边向左边转移水:

给左边的水直接算入他的答案
给自己留下的水直接计入自己的答案

因此我们可以写出代码:



 ll ny=qpow(n-1,mod-2);
 //初始化
f[1][a[1]]=1;
for(int i=1;i<=n;++i){
    //右向左倒水
    for(int j=1;j<i;++j){
        //分情况记入 j 的答案和 i 的答案
        for(int k=1;k<=min(a[j],a[i]);++k){
            ans[j]+=f[i][k]*k%mod*ny%mod;mo(ans[j]);
        }
        for(int k=a[j]+1;k<=a[i];++k){
            ans[j]+=f[i][k]*a[j]%mod*ny%mod;mo(ans[j]);
            ans[i]+=f[i][k]*ny%mod*(k-a[j])%mod;mo(ans[i]);
        }
    }
    // 左向右倒水
    for(int j=i+1;j<=n;++j){
        //分情况计入 j 的 f 数组和 i 的答案
        for(int k=1;k<=min(a[i],a[j]);++k){
            f[j][k]+=f[i][k]*ny%mod;mo(f[j][k]);
        }
        for(int k=a[j]+1;k<=a[i];++k){
            f[j][a[j]]+=f[i][k]*ny%mod;mo(f[j][a[j]]);
            ans[i]+=f[i][k]*ny%mod*(k-a[j])%mod;mo(ans[i]);
        }
    }
}



观察上面的转移,我们在确定 $ i,j $ 之后又枚举了一维 $ k $ 表示当前的水量,但实际上对于一个 $ j $ 我们只关心 $ a_j $ 与 $ k $ 的大小关系,所以我们就可以前缀和优化一下。我们分别来看一下这四种情况怎么优化:

1、左倒右, $ k \le a_j $

就是给 $ ans_j $ 加上了 $ \sum_{k=1}^{a_j} f_{i,k} \times k \times ny $ 。

2、左倒右, $ k \gt a_j $

$ ans_j ← \sum_{ k = a_j + 1 }^{ a_i } f_{i,k} \times a_j \times ny $

$ ans_i ← ( \sum_{ k =a_j +1 }^{ a_i } f_{i,k} \times k - \sum_{ k =a_j +1}^{ a_i } f_{i,k} \times a_j ) \times ny $

3、右倒左, $ k \le a_j $

就是原始式子,没什么可改写的。

4、右倒左, $ k \gt a_j $

$ f_{j,a_j} ← \sum_{ k = a_j+1 }^{ a_i } f_{i,k} \times ny $ (注意到 $ f $ 数组只用了一个,所以可以省略一维,用 $ f_j $ 表示 $ f_{j,a_j} $ )

$ ans_i ← ( \sum_{ k =a_j +1 }^{ a_i } f_{i,k} \times k - \sum_{ k =a_j +1}^{ a_i } f_{i,k} \times a_j ) \times ny $ (和上面2中的那个一样)

根据上面这些式子,我们需要对于每个 $ i $ 维护好 $ F_j = \sum_{k=1}^{ j } f_{i,k} 和 G_j = \sum_{k=1}^{j} f_{i,k} \times k $ 即可处理除了 3 之外的所有式子

一共 4 个式子我们已经搞了 3 个,就剩 3 中的那个我们无法优化。

那么我们换种思路,可以观察到 3 中的转移第二维 $ k $ 不变,所以从这方面入手,我们画图来说:

image

以此图来表示 $ f_{i,k} $ 。

对于 3 操作来说, $ f_{i,k} $ 会对 $ f_{j,k} ( j \gt i ) $ 造成贡献 ,换句话说, $ f_{i,k} $ 由 $ f_{j,k} ( j \lt i ) $ 转移而来,即图中绿色的地方由红色的地方转移而来。

image

所以用一个横向的前缀和 $ FF $ 即可。

可以写出代码:


//prea 表示 F  pree 表示 横向前缀和 

f[1]=1;
for(int i=1;i<=n;++i){
    for(int j=1;j<=a[i];++j){
        prea[j]=prea[j-1]+pree[j]*ny%mod;mo(prea[j]);
        g[j]=g[j-1]+pree[j]*ny%mod*j%mod;mo(g[j]);
    }
    prea[a[i]]+=f[i];mo(prea[a[i]]);g[a[i]]+=f[i]*a[i]%mod;mo(g[a[i]]);
    for(int j=1;j<i;++j){
        ans[j]+=g[min(a[i],a[j])]*ny%mod;mo(ans[j]);
        if(a[i]>a[j])ans[j]+=(prea[a[i]]-prea[a[j]]+mod)%mod*a[j]%mod*ny%mod;mo(ans[j]);
        if(a[i]>a[j])ans[i]+=(g[a[i]]-g[a[j]]-(prea[a[i]]-prea[a[j]]+mod)%mod*a[j]%mod+mod*10)%mod*ny%mod;mo(ans[i]);
    }
    for(int j=i+1;j<=n;++j){
        if(a[i]>a[j]){
            f[j]+=(prea[a[i]]-prea[a[j]]+mod)%mod*ny%mod;mo(f[j]);
            ans[i]+=(g[a[i]]-g[a[j]]-(prea[a[i]]-prea[a[j]]+mod)%mod*a[j]%mod+mod*10)%mod*ny%mod;mo(ans[i]);
        }
    }
    for(int j=1;j<=a[i];++j){
        pree[j]+=(prea[j]-prea[j-1]+mod)%mod;mo(pree[j]);
    }
}

优化了这么多,我们已经可以 $ O(n^2) $ 解决这个题了,但是还是过不去,我们继续通过画图分析各个转移的本质:

1、首先看看 $ f $ 与 $ f $ 之间的转移

上面的 3 和 4 中的第一个 是 $ f $ 与 $ f $ 之间的转移。

对于第 3 个转移已经通过上面那张图展现出来了,接下来看看 4 中的第一个:

image

就像上图一样。对于被绿色圈起来的那个地方,会被前面所有比他高的转移过来。

那么整个过程就可以用下图表示:

image

考虑怎么快速维护这个横向的前缀和,对于黄色箭头表示的横向转移,看看他的式子:

$ f_{i,k} = FF_{i-1,k} \times ny $

$ FF_{i,k} = FF_{i-1,k} + f_{i,k} $

但是后面这个式子可以这么写: $ FF_{i,k} = FF_{i-1,k} \times (ny+1) $

那这是什么,这是区间乘。

那红色的那个是不是更好说了,区间查询,单点加。

接下来我们都只需要维护横向的前缀和就行了, $ F_{i,k} = \sum_{j=1}^{i} f_{j,k} $ , $ G_{i,k} = \sum_{j=1}^{i} f_{j,k} \times k $

所以直接用线段树维护 $ F 、 G $ 即可

2、给别的杯子倒水时给自己留下的水

首先这种情况不用区分前后,我们直接看全局:

式子: $ \sum { f_{i,k} } \times ( k \times n - \sum_{j=1}^{n} \min(k,a_j) ) \times ny $

image

模拟一下中间有箭头的那列,我们先枚举 $ k $

image

上面红色的表示 $ min(k,a_j) $

image

当 $ k $ 上升时整体会跟着上升

image

当 $ k $ 上升到一定程度时有一些列已经跟不上了。

这个过程仍然可以用线段树维护。

首先对于每个 $ k $ ,我们预处理出 $ \sum { min(k,a_j) } $ 。

对于一开始的式子: $ \sum { f_{i,k} } \times ( k \times n - \sum_{j=1}^{n} \min(k,a_j) ) \times ny $

把它分为

$ n \times \sum { f_{i,k} } \times k $ 和 $ \sum { f_{i,k} } \times \sum \min(k,a_j) $

前者就是 用 $ G $ 可求得 ,后者仍然可以用线段树来维护。

3、被别的杯子倒回来水

image

竟然和第一步一样,只需要在第一遍从左向右扫过去的时候记一下此时的 $ \sum_{j=1}^{a_i} G_{i,j} $ 和 $ \sum_{ j = a_i+1 }^{n} F_{i,j} $ ,即可用最后全局的 $ \sum G 、\sum F $ 减去刚才记的东西就是后面黄色的和红色的部分。

到此为止 我们已经把一个 $ O(n^3) $ 的暴力DP优化到了 $ O(nlog(n)) $ 。

再仔细思考一下整个过程,其实就是带权矩形面积和。

闲话:

9G 听了 Qyun 的几句话之后说:“我会 $ O(n^2) $ 的了,我现在感觉 豁然开朗!”。然后我说了句:“别急,你还得豁然开朗好几次呢。”

/*
GGrun
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define ps push_back
#define mk make_pair
#define fi first
#define se second
const int N=1e6+10,M=2e5+10,inf=0x3f3f3f3f;
const ll linf=0x3f3f3f3f3f3f3f3f,mod=998244353;
inline int read(){
	char c=getchar();int x=0;
	while(!isdigit(c))c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x;
}
inline ll qpow(ll x,ll y){
	ll ans=1;
	while(y){
		if(y&1)ans=ans*x%mod;
		x=x*x%mod;y>>=1;
	}
	return ans;
}
#define mo(x) (x>=mod?x-=mod:0)
ll prsum[M],pre[M],preny[M];
int a[M],b[M],n;
struct jp{
	ll tagc,tagj1,tagj2;
	inline void operator +=(const jp&x){
		tagc=tagc*x.tagc%mod;
		tagj1=(tagj1+x.tagj1+x.tagc*tagj1)%mod;
		tagj2=(tagj2+x.tagj2+x.tagc*tagj2)%mod;
	}
}tag[N];
struct jj{
	ll sum,f,g,ans;
	inline void operator +=(const jp&x){
		f=(f*x.tagc+x.tagj1)%mod;
		g=(g*x.tagc+x.tagj2)%mod;
		ans=(ans*x.tagc+x.tagj1*sum)%mod;
	}
	inline void operator +=(const jj&x){
		f+=x.f;g+=x.g;ans+=x.ans;
		mo(f),mo(g),mo(ans);
	}
}tr[N];
inline void jian(int k,int l,int r){
	tag[k].tagc=1;
	if(l==r)return (void)(tr[k].sum=prsum[l]);
	int mid=l+r>>1;
	jian(k<<1,l,mid),jian(k<<1|1,mid+1,r);
	tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
}
inline void down(int k){
	if(tag[k].tagc!=1||tag[k].tagj1||tag[k].tagj2){
		tr[k<<1]+=tag[k],tr[k<<1|1]+=tag[k],tag[k<<1]+=tag[k],tag[k<<1|1]+=tag[k];
		tag[k]={1,0,0};
	}
}
inline void up(int k){
	tr[k].f=tr[k<<1].f+tr[k<<1|1].f,tr[k].g=tr[k<<1].g+tr[k<<1|1].g,tr[k].ans=tr[k<<1].ans+tr[k<<1|1].ans;
	mo(tr[k].f);mo(tr[k].g);mo(tr[k].ans);
}
inline void add(int k,int l,int r,int L,int R){
	if(L<=l&&r<=R){
		jp x={preny[1]+1,0,0};
		tr[k]+=x,tag[k]+=x;
		return;
	}
	down(k);
	int mid=l+r>>1;
	if(L<=mid)add(k<<1,l,mid,L,R);
	if(R>mid)add(k<<1|1,mid+1,r,L,R);
	up(k);
}
inline void add(int k,int l,int r,int pos,int v1,int v2){
	if(l==r){jp x={1,v1,v2};tr[k]+=x,tag[k]+=x;return;}
	down(k);
	int mid=l+r>>1;
	pos<=mid?add(k<<1,l,mid,pos,v1,v2):add(k<<1|1,mid+1,r,pos,v1,v2);
	up(k);
}
inline jj ask(int k,int l,int r,int L,int R){
	if(L<=l&&r<=R){
		return tr[k];
	}
	down(k);
	int mid=l+r>>1;
	jj ans={0,0,0,0};
	if(L<=mid)ans=ask(k<<1,l,mid,L,R);
	if(R>mid)ans+=ask(k<<1|1,mid+1,r,L,R);
	return ans;
}
ll ans[M],tpg[M],tpf[M];
int main(){

	freopen("bottle.in","r",stdin);
	freopen("bottle.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	n=read();
	for(int i=1;i<=n;++i)
		a[i]=read(),b[i]=a[i];
	sort(b+1,b+1+n);
	ll ny=qpow(n-1,mod-2);
	pre[0]=preny[0]=1;
	for(int i=1,j=1;i<=n;++i){
		pre[i]=pre[i-1]*(n-1)%mod,preny[i]=preny[i-1]*ny%mod;
		prsum[i]=prsum[i-1]+n-j+1;
		mo(prsum[i]);
		while(j<=n&&b[j]==i)++j;
	}
	jian(1,1,n);
	add(1,1,n,a[1],1,(ll)a[1]%mod);
	ans[1]=((ll)a[1]*n%mod-prsum[a[1]]+mod+ans[1])%mod*ny%mod;
	jj op=ask(1,1,n,1,a[1]);
	tpg[1]=op.g;tpf[1]=(tr[1].f-op.f+mod)%mod;
	for(int i=2;i<=n;++i){
		op=ask(1,1,n,1,a[i]);
		op.f=op.f*ny%mod,op.g=op.g*ny%mod,op.ans=op.ans*ny%mod;
		if(a[i]<n){
			op.g+=(tr[1].f*ny-op.f+mod)%mod*a[i]%mod;
			op.ans+=(tr[1].f*ny-op.f+mod)%mod*prsum[a[i]]%mod;
			mo(op.g);mo(op.ans);
		}
		ans[i]+=(op.g*n-op.ans+mod)%mod*ny%mod;
		ll zan=tr[1].f;
		add(1,1,n,1,a[i]);
		add(1,1,n,a[i],(zan*ny-op.f+mod)%mod,(zan*ny-op.f+mod)%mod*a[i]%mod);
		op=ask(1,1,n,1,a[i]);
		tpg[i]=op.g;tpf[i]=(tr[1].f-op.f+mod)%mod;
	}
	for(int i=1;i<=n;++i){
		jj op=ask(1,1,n,1,a[i]);
		op.g-=tpg[i];op.g=(op.g*ny%mod+mod)%mod;
		ans[i]+=op.g;mo(ans[i]);
		if(a[i]<n){
			op.f=(tr[1].f-op.f+mod)%mod;
			op.f=(op.f-tpf[i]+mod)%mod*ny%mod;
			ans[i]+=op.f*a[i]%mod;mo(ans[i]);
		}
	}
	for(int i=1;i<=n;++i)
		cout<<(ans[i]+mod)%mod<<'\n';
}

\[END \ \ \ OF \ \ \ T4 \]

posted @ 2024-10-16 10:48  lzrG23  阅读(111)  评论(17编辑  收藏  举报