寒假集训

未做清单:
花园(去学矩阵!)
FWT板子

Day 1

前言:为什么今天右眼皮总跳……拜托一定要发生点好事啊

今天的调试:

  1. 方差:首先,updatereturn。其次,没看到“实数”。最后,推的式子是对的,但统计答案时出错了。怒调半小时(?)
  2. 灯泡:哦一遍过
  3. 矩阵取数:一是没发现数据极大会爆 long long,二是 “多测”没“清空”
  4. 子树和:ans 初值没赋极小值。

线段树

作业链接

1.1 可能是我人生中的第 n 道紫题?

本来没打算写的,直到看到同机房的人都写了……

但感觉可以二维数点

b 不是 p 的祖先,就是 p 的子树中节点。所以可以给 b 掰两半统计:处理 p 的祖先满足 disbdispk 的个数,此时 c 可以在 p 的子树中任意取;二维数点处理满足 dfnb[dfnp+1,dfnp+sizp1],depb[depp+1,depp+k] 的数,对于每个 b 的取值,c 可以在 b 的子树中任意取

然后大概就做完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=6e5+5;
int n,q;
vector <int> tr[N];
int ans[N];
struct node { int id,val,lim; };
vector <node> p[N],ist[N];
int ea[N];

int siz[N],dfn[N],tme,dep[N];
void dfs(int x,int _fa)
{
	siz[x]=1,dfn[x]=++tme,dep[x]=dep[_fa]+1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs(v,x);
		siz[x]+=siz[v];
	}
	ist[dfn[x]].push_back({dep[x],siz[x]-1,0});
}
int lowbit(int x) { return x&-x; }
void add(int x,int y)
{
	while (x<N)
	{
		ea[x]+=y;
		x+=lowbit(x);
	}
}
int sum(int x)
{
	int res=0;
	while (x)
	{
		res+=ea[x];
		x-=lowbit(x); 
	}
	return res;
}
void solve()
{
	for (int i=1;i<=n;i++)
	{
		int _size1=ist[i].size();
		for (int j=0;j<_size1;j++) add(ist[i][j].id,ist[i][j].val);
		
		int _size2=p[i].size();
		for (int j=0;j<_size2;j++) ans[p[i][j].id]+=p[i][j].val*sum(p[i][j].lim);
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>q;
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	dfs(1,0);
	for (int i=1,u,k;i<=q;i++)
	{
		cin>>u>>k;
		p[dfn[u]+siz[u]-1].push_back({i,1,dep[u]+k});
		p[dfn[u]+siz[u]-1].push_back({i,-1,dep[u]});
		p[dfn[u]].push_back({i,-1,dep[u]+k});
		p[dfn[u]].push_back({i,1,dep[u]});
		ans[i]+=(siz[u]-1)*min(k,dep[u]-1);
	}

	solve();
	for (int i=1;i<=q;i++) cout<<ans[i]<<"\n";
	return 0;
}

线段树合并板子

wiki链接

顾名思义,就是有一颗新的线段树,这棵树上的每个节点是原来两棵线段树的相应结点合并后的值。需要用到动态开点

对于这道板子,不理解为什么要线段树合并 于是乎研究了一下sxht大巨的代码

不喜欢这道题,因为分屏有问题
下面将“不同赈灾粮种类”称为“不同颜色”

这道题显然要树上差分。这大概算是一个……颜色的单修区查,所以要在每个节点建立一棵线段树,维护节点 u[l,r] 每个颜色出现次数的最大值 sum ,及 sum 对应的颜色编号 col

而这若干棵线段树维护的都是差分信息,所以最后还需要把这些线段树以“前缀和”的方式加在一起——也就是线段树合并

然后就完了(喜

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m;
vector <int> tr[N];
struct node{
	int ls,rs;
	int sum,col;
}smt[N*50];
int rt[N],cnt;
int ans[N];

void push_up(int u)
{
	int ls=smt[u].ls,rs=smt[u].rs;
	if (smt[ls].sum>=smt[rs].sum)
	{
		smt[u].sum=smt[ls].sum;
		smt[u].col=smt[ls].col;
	}
	else
	{
		smt[u].sum=smt[rs].sum;
		smt[u].col=smt[rs].col;
	}
}
void update(int &u,int l,int r,int pos,int x)
{
	if (!u) u=++cnt;
	if (l==r)
	{
		smt[u].sum+=x,smt[u].col=pos;
		return ;
	}
	
	int mid=(l+r)>>1;
	if (mid>=pos) update(smt[u].ls,l,mid,pos,x);
	else update(smt[u].rs,mid+1,r,pos,x);
	push_up(u);
}
int merge(int u,int v,int l,int r)
{
	if (!u||!v) return u+v;
	if (l==r) { smt[u].sum+=smt[v].sum; return u; }
	
	int mid=(l+r)>>1;
	smt[u].ls=merge(smt[u].ls,smt[v].ls,l,mid);
	smt[u].rs=merge(smt[u].rs,smt[v].rs,mid+1,r);
	push_up(u);
	return u;
}
int dep[N],st[N][25];
void dfs(int x,int _fa)
{
	dep[x]=dep[_fa]+1;
	st[x][0]=_fa;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs(v,x);
	}
}
void init()
{
	dfs(1,0);
	for (int j=1;j<=20;j++)
	for (int i=1;i<=n;i++) st[i][j]=st[st[i][j-1]][j-1];
}
int lca(int x,int y)
{
	if (dep[x]<dep[y]) swap(x,y);
	int delta=dep[x]-dep[y],lg=0;
	while (delta)
	{
		if (delta&1) x=st[x][lg];
		delta>>=1,lg++;
	}
	if (x==y) return x;
	
	for (int i=20;i>=0;i--)
	{
		if (st[x][i]==st[y][i]) continue;
		x=st[x][i],y=st[y][i];
	}
	return st[x][0];
}
void calc(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		calc(v,x);
		rt[x]=merge(rt[x],rt[v],1,N);
	}
	if (smt[rt[x]].sum) ans[x]=smt[rt[x]].col;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	
	init();
	int x,y,z;
	while (m--)
	{
		cin>>x>>y>>z;
		int ll=lca(x,y);
		update(rt[x],1,N,z,1);//树上差分
		update(rt[y],1,N,z,1);
		update(rt[ll],1,N,z,-1);
		update(rt[st[ll][0]],1,N,z,-1);
	}
	calc(1,0);//“前缀和”
	for (int i=1;i<=n;i++) cout<<ans[i]<<"\n";
	return 0;
}

DP

作业链接

直接上强度吧

不上了

2.1 灯泡

看到标题,我们知道这是区间 dp

设状态 fi,j,0/1 表示已经关掉 [i,j] 内的所有灯,目前处于这个区间的最左/右端的最小耗电量

那么显然有状态转移方程(沿同一方向走或折返换向)

fi,j,0=min(fi+1,j,0+(posi+1posi)×(sumn(sumjsumi),fi+1,j,1+(posjposi)×(sumn(sumjsumi)))

fi,j,1=min(fi,j1,1+(posjposj1)×(sumn(sumj1sumi1)),fi,j1,0+(posjposi)×(sumn(sumj1sumi1)))

DP的魅力——超短代码
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=52;
int n,c;
int pos[N],sum[N];
int f[N][N][2];

signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	memset(f,0x3f,sizeof f);
	cin>>n>>c;
	for (int i=1;i<=n;i++) { cin>>pos[i]>>sum[i]; sum[i]+=sum[i-1]; }
	f[c][c][0]=f[c][c][1]=0;
	for (int len=2;len<=n;len++)
	for (int i=1;i+len-1<=n;i++)
	{
		int j=i+len-1;
		f[i][j][0]=min(f[i+1][j][0]+(pos[i+1]-pos[i])*(sum[n]-(sum[j]-sum[i])),f[i+1][j][1]+(pos[j]-pos[i])*(sum[n]+sum[i]-sum[j]));
		f[i][j][1]=min(f[i][j-1][1]+(pos[j]-pos[j-1])*(sum[n]+sum[i-1]-sum[j-1]),f[i][j-1][0]+(pos[j]-pos[i])*(sum[n]+sum[i-1]-sum[j-1])); 
	}
	cout<<min(f[1][n][0],f[1][n][1]);
	return 0;
}

2.2 矩阵取数游戏

哎我真服了一个绿题的高精需求直接卡我一晚上

发现每一行都可以独立计算,把每一行的最大价值相加就是最后的答案

对于每一行,因为每次选数只能从两头选,所有没被选的数一定会形成一个连续的区间。于是乎,设状态 fi,j 表示对于正在处理的这一行,不选 [i,j] 的数的最大得分。

显然有状态转移方程
fi,j=max(fi,j+1+aj×2mlen,fi1,j+ai1×2mlen)

然后就做完了

然后就又WA又TLE了

打开题解,发现要高精 直接放弃

但是能用int128解决(这我也不会啊)

关于 int128

定义是 __int128 (俩下划线?)最大范围可到21271,要是开 unsigned 范围更大

然后,这个东西只能四则运算,只能快读快写。。。

void input(__int128 &s)
{
	char c=' ';
	while (c>'9'||c<'0') c=getchar();
	while (c>='0'&&c<='9')
	{
		s=s*10+(c-'0');
		c=getchar();
	}
}
void output(__int128 x)
{
	if (x==0) return ;
	if (x) output(x/10);
	putchar(x%10+'0');
}

(好麻烦)

然后WA。为什么呢,因为“多测”忘“清空”了。

史诗级代码
#include <bits/stdc++.h>
using namespace std;
const int N=81;
int n,m,a[N];
__int128 p[92]={1};
__int128 f[N][N];
__int128 ans,sum;

inline void input(__int128 &s)
{
	char c=' ';
	while (c>'9'||c<'0') c=getchar();
	while (c>='0'&&c<='9')
	{
		s=s*10+(c-'0');
		c=getchar();
	}
}
inline void output(__int128 x)
{
	if (x==0) return ;
	if (x) output(x/10);
	putchar(x%10+'0');
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1;i<=90;i++) p[i]=p[i-1]<<1;
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=m;j++) cin>>a[j];
		
		memset(f,0,sizeof f);
		f[1][m-1]=2*a[m];
		f[2][m]=2*a[1];
		for (int len=m-2;len>=0;len--)
		for (int i=1;i+len-1<=m;i++)
		{
			int j=i+len-1;
			f[i][j]=max(f[i][j+1]+a[j+1]*p[m-len],f[i-1][j]+a[i-1]*p[m-len]);
		}
		ans=0;
		for (int i=1;i<=m;i++) ans=max(ans,f[i][i-1]);
		sum+=ans;
	}
	if (sum==0) puts("0");
	else output(sum);
	return 0;
}

2.3 最大子树和

没啥好说的。设状态 fu 表示 u 的子树内选取若干个连通的点的最大和(包括点 u),最后答案就是 ans=max{fu}

但是这题有负数负数负数负数负数

看到最大值 ans 初值赋 0xc1 ,看到最小值 ans 初值赋 0x3f

被黄题爆切了……
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=16005;
int n,v[N];
vector <int> tr[N];
int f[N],ans=0xc1c1c1c1c1c1c1c1;

void dfs(int x,int _fa)
{
	f[x]=v[x];
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs(v,x);
		if (f[v]>0) f[x]+=f[v];
	}
	ans=max(ans,f[x]);
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n;
	for (int i=1;i<=n;i++) cin>>v[i];
	for (int i=1,a,b;i<n;i++)
	{
		cin>>a>>b;
		tr[a].push_back(b),tr[b].push_back(a);
	}
	dfs(1,0),cout<<ans;
	return 0;
}

Day 2

RMQ啥的

作业链接

1.1 被绿题爆切了

第一眼,感觉要处理出所有的 aix

第二眼,感觉要找到 aix 在原数组中的位置

因为忘记 map 的存在,所以以为实现第二步就需要 O(n2) ,遂放弃

全剧终

咳咳,完成上两步后需要一些其他的操作。设状态 fi 表示 fi=max{pos(ajx)},j[1,i] ,其中 pos(x) 表示值 x 在原数组中的下标。

这样了话,对于区间 [l,r] ,若 fr[l,r] ,那么肯定不存在那个值。

啊啊啊好难讲,还是得感性理解……但感性理解太不严谨了

反正,然后就完了

#incIude <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,q,x;
int f[N];
map <int,int> mp;

signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>q>>x;
	for (int i=1,a;i<=n;i++)
	{
		cin>>a;
		mp[a^x]=i;
		f[i]=max(f[i-1],mp[a]);
	}
	
	int l,r;
	while (q--)
	{
		cin>>l>>r;
		if (l<=f[r]) cout<<"yes"<<"\n";
		else cout<<"no"<<"\n"; 
	}
	return 0;
}

1.2 不是怎么又被绿题爆切了

显然的树上差分+LCA。但是……

归来仍不会树上差分
咳咳,解决树上差分这道题就happy ending了!
首先,这不是我刚开始想的那样头加尾减,因为它会有若干个“尾”,无法处理。所以应是尾加头减,dfs时回溯累加!
最后回溯到lca(u,v)时,它会多加两次,所以它要减减;它的父节点不需要多加,所以它的父节点也需要减减
也就是说,对于形如 u->lca(u,v)->v 的一条路径,它的差分应该是 difu+1,difv+1,diflca(u,v)1,diffalca(u,v)1

然后就完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
int n,k;
vector <int> tr[N];
int dep[N],st[N][30];
int dif[N],ans;

void dfs(int x,int _fa)
{
	st[x][0]=_fa,dep[x]=dep[_fa]+1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs(v,x);
	}
}
void init()
{
	dfs(1,0);
	for (int j=1;j<=20;j++)
	for (int i=1;i<=n;i++) st[i][j]=st[st[i][j-1]][j-1];
}
int lca(int x,int y)
{
	if (dep[x]<dep[y]) swap(x,y);
	
	int lg2=0,delta=dep[x]-dep[y];
	while (delta)
	{
		if (delta&1) x=st[x][lg2];
		delta>>=1,lg2++;
	}
	if (x==y) return x;
	
	for (int i=20;i>=0;i--)
	{
		if (st[x][i]==st[y][i]) continue;
		x=st[x][i],y=st[y][i];
	}
	return st[x][0];
}
void dfs2(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs2(v,x);
		dif[x]+=dif[v];
	}
	ans=max(ans,dif[x]);
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>k;
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	init();
	
	for (int i=1,s,t;i<=k;i++)
	{
		cin>>s>>t;
		int ll=lca(s,t);
		dif[s]++,dif[t]++,dif[ll]--,dif[st[ll][0]]--;
	}
	dfs2(1,0),cout<<ans;
	return 0;
}

各种DP

作业链接

2.1 随机抽取一道换根DP

首先,大概手推了下,发现这就是换根dp。设状态 f1u,j 表示 u 的子树中所有与 u 距离不超过 j 的节点的权值和。

跑完一遍,发现只有根节点统计到位,果断开始换根

设状态 f2u,j 表示在整棵树中,与 u 距离不超过 j 的节点的权值和。

手模后,会发现转移式子:

f2v,j=f1v,j+f2u,j1f1v,j2

然后就做完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,k;
vector <int> tr[N];
int c[N];
int f1[N][25],f2[N][25];

int fa[N];
void dfs1(int x,int _fa)
{
	for (int i=0;i<=k;i++) f1[x][i]=c[x];
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs1(v,x);
		for (int j=1;j<=k;j++) f1[x][j]+=f1[v][j-1];
	}
	for (int i=1;i<=k;i++) f2[x][i]=f1[x][i];
}
void dfs2(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		for (int j=2;j<=k;j++) f2[v][j]+=f2[x][j-1]-f1[v][j-2];
		f2[v][1]+=f1[x][0];
		dfs2(v,x);
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>k;
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v);
		tr[v].push_back(u);
	}
	for (int i=1;i<=n;i++) cin>>c[i];
	
	dfs1(1,0);
	dfs2(1,0);
	for (int i=1;i<=n;i++) cout<<f2[i][k]<<"\n";
	return 0;
}

2.2 随机抽取一道数位DP

显然的数位DP

因为方案数与前一个选的数密切相关,所以设状态 fpos,num 表示处理到第 pos 位,前一个数填的是 num 的方案数。

直接记搜就完了。但是前导0真的很烦很烦哎

所以还要单独记个东西来存是否有前导0

然后莫名其妙调了我一个小时。。。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=12;
int a,b;
int la[N],len;
int f[N][N];

int dfs(int pos,int num,int lim,int flag)
{
	if (!pos) return 1;
	if (!lim&&f[pos][num]!=-1) return f[pos][num];

	int res=0,up=9;
	if (lim) up=la[pos];
	for (int i=0;i<=up;i++)
	{
		if (abs(i-num)<2) continue;
		if (flag&&!i) res+=dfs(pos-1,-2,lim&&i==up,2);
		else res+=dfs(pos-1,i,lim&&i==up,0);
	}

	if (!lim&&!flag) f[pos][num]=res;
	return res;
}
int solve(int x)
{
	len=0;
	while (x)
	{
		la[++len]=x%10;
		x/=10;
	}
	return dfs(len,-2,1,1);
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);

	memset(f,-1,sizeof f);
	cin>>a>>b;
	cout<<solve(b)-solve(a-1);
	return 0;
}

2.3 随机抽取一道状压DP

看到环,把链复制成两份就行了(甚至这题都不用完全搞两份?)


Day 3

FWT

作业链接&wiki链接

听不懂听不懂听不懂听不懂听不懂

FWT 是用于解决对下标进行位运算卷积问题的方法。

Ck=Σij=k Ai×Bj

(注意上述式子的 可能代表按位与、按位或、按位异或)

一、或卷积

Ck=Σi|j=k Ai×Bj

构造 T(A)i=ΣjiAj(这里的子集意为二进制下的 j|i=i),那么有

T(A)×T(B)=jiAj×kiBk

=jikiAj×Bk

=(j|k)iAj×Bk=T(C)

A0 表示 A 中最高位为 0 ,即序列前 2n1 的部分序列;设 A1 表示 A 中最高位为 1 ,即后 2n1的序列。那么会有递推式:

T(A)=merge(T(A0),T(A0)+T(A1))

(因为最高位为 1 的序列一定不会是最高位为 0 的子集,相反,最高位为 0 的可以是最高位为 1 的子集)

已知 T(A0)T(A1) ,就可以求出 A0,A1 。有 T 的逆变换 IT

IT(A)=merge(IT(A0),IT(A1)IT(A0))

然后经过上述两次变换,就可以求出 C

二、与卷积

Ck=Σi&j=k Ai×Bj

与或卷积同理,有 T(C)i=T(A)i×T(B)i

然后有 T(A)=merge(T(A0)+T(A1),T(A1))

然后有 IT(A)=merge(IT(A0)IT(A1),IT(A1))

三、异或卷积

Ck=Σij=k Ai×Bj

我们需要构造出一个 T(A)i=T(B)i×T(C)i

1.1 板子

不会不会

树链剖分

作业链接

……一年前其实学过,但学和会不是一个事情

因为树链剖分 dfn 序的性质,它可以在路径上维护,也可以在子树上维护。

一棵树可以被分成不超过 log2n 条链,这也就保证了树剖的复杂度

2.1 板子

区修区查,这道题显然要用线段树维护区间和(显然,这里的区间指的是 dfn 序的区间)

若是在路径上维护,就要用树剖性质将该路径分成若干条链(每条链中的 dfn 序肯定是联通的),再用线段树分别维护这若干个 dfn 区间就行了。因为一条路径中最多有 log n 条链,所以复杂度是有保证的

那么,先跑两遍 dfs 预处理出重儿子、子树大小等信息,再树剖在路径/子树上维护权值和就行了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,q,rt,mod;
int c[N],val[N];
vector <int> tr[N];
struct segment_tree
{
	struct node {
		int l,r;
		int sum,tag;
	}smt[N<<2];
	void push_up(int id) { smt[id].sum=smt[id<<1].sum+smt[(id<<1)|1].sum; }
	void push_down(int id)
	{
		if (!smt[id].tag) return ;
		int ls=id<<1,rs=(id<<1)|1;
		smt[ls].sum=(smt[ls].sum+(smt[ls].r-smt[ls].l+1)*smt[id].tag%mod)%mod;
		smt[rs].sum=(smt[rs].sum+(smt[rs].r-smt[rs].l+1)*smt[id].tag%mod)%mod;
		smt[ls].tag+=smt[id].tag,smt[rs].tag+=smt[id].tag,smt[id].tag=0;
	}
	void build(int id,int l,int r)
	{
		smt[id].l=l,smt[id].r=r;
		if (l==r) { smt[id].sum=val[l]%mod; return ; }
		
		int mid=(l+r)>>1;
		build(id<<1,l,mid);
		build((id<<1)|1,mid+1,r);
		push_up(id);
	}
	void update(int id,int l,int r,int x)
	{
		if (smt[id].l>=l&&smt[id].r<=r)
		{
			smt[id].sum=(smt[id].sum+x*(smt[id].r-smt[id].l+1)+mod)%mod;
			smt[id].tag+=x;
			return ;
		}
		
		push_down(id);
		int mid=(smt[id].l+smt[id].r)>>1;
		if (mid>=l) update(id<<1,l,r,x);
		if (mid+1<=r) update((id<<1)|1,l,r,x);
		push_up(id);
	}
	int query(int id,int l,int r)
	{
		if (smt[id].l>=l&&smt[id].r<=r) return smt[id].sum;
		
		push_down(id);
		int mid=(smt[id].l+smt[id].r)>>1,res=0;
		if (mid>=l) res=(res+query(id<<1,l,r)+mod)%mod;
		if (mid+1<=r) res=(res+query((id<<1)|1,l,r)+mod)%mod;
		return res;
	}
}Tr;

int dep[N],siz[N],son[N],fa[N];
int dfn[N],tme,top[N];
void dfs1(int x,int _fa)
{
	dep[x]=dep[_fa]+1,fa[x]=_fa,siz[x]=1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs1(v,x);
		if (siz[v]>siz[son[x]]) son[x]=v;
		siz[x]+=siz[v];
	}
}
void dfs2(int x,int _top)
{
	dfn[x]=++tme,top[x]=_top,val[dfn[x]]=c[x];
	if (!son[x]) return ;
	dfs2(son[x],_top);
	
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==fa[x]||v==son[x]) continue;
		dfs2(v,v);
	}
}
void update1(int x,int y,int z)
{
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		Tr.update(1,dfn[top[x]],dfn[x],z%mod);
		x=fa[top[x]];
	}
	if (dep[x]<dep[y]) swap(x,y);
	Tr.update(1,dfn[y],dfn[x],z%mod);
}
int query1(int x,int y)
{
	int res=0;
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		res=(res+Tr.query(1,dfn[top[x]],dfn[x]))%mod;
		x=fa[top[x]];
	}
	if (dep[x]<dep[y]) swap(x,y);
	res=(res+Tr.query(1,dfn[y],dfn[x]))%mod;
	return res;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>q>>rt>>mod;
	for (int i=1;i<=n;i++) cin>>c[i]; 
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	
	dfs1(rt,0),dfs2(rt,rt);
	Tr.build(1,1,n);
	int op,x,y,z;
	while (q--)
	{
		cin>>op;
		if (op==1) { cin>>x>>y>>z; update1(x,y,z); }
		else if (op==2) { cin>>x>>y; cout<<query1(x,y)<<"\n"; }
		else if (op==3) { cin>>x>>z; Tr.update(1,dfn[x],dfn[x]+siz[x]-1,z); }
		else { cin>>x; cout<<Tr.query(1,dfn[x],dfn[x]+siz[x]-1)<<"\n"; }
	}
	return 0;
}

随机抽取一道树链剖分

这不就板子吗?调一些奇奇怪怪的错误就过了

fjj休想碰我羽绒服帽子上的毛
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5; 
int n,q;
vector <int> tr[N];
struct Segment_tree
{
	struct node{
		int l,r;
		int sum,tag;//-1:变0 1:变1 
	}tr[N<<2];
	void push_up(int id) { tr[id].sum=tr[id<<1].sum+tr[id<<1|1].sum; }
	void push_down(int id)
	{
		if (!tr[id].tag) return ;
		int ls=id<<1,rs=id<<1|1;
		if (tr[id].tag==1)
		{
			tr[ls].sum=tr[ls].r-tr[ls].l+1;
			tr[rs].sum=tr[rs].r-tr[rs].l+1;
			tr[ls].tag=tr[rs].tag=1;
		}
		else
		{
			tr[ls].sum=tr[rs].sum=0;
			tr[ls].tag=tr[rs].tag=-1;
		}
		tr[id].tag=0;
	}
	void build(int id,int l,int r)
	{
		tr[id].l=l,tr[id].r=r;
		if (l==r) return ;
		int mid=(l+r)>>1;
		build(id<<1,l,mid),build(id<<1|1,mid+1,r);
	}
	void update(int id,int l,int r,int w)
	{
		if (tr[id].l>=l&&tr[id].r<=r) { tr[id].sum=(tr[id].r-tr[id].l+1)*w; tr[id].tag=(w?1:-1); return ; }
		
		push_down(id);
		int mid=(tr[id].l+tr[id].r)>>1;
		if (mid>=l) update(id<<1,l,r,w);
		if (mid+1<=r) update(id<<1|1,l,r,w);
		push_up(id); 
	}
	int query(int id,int l,int r)
	{
		if (tr[id].l>=l&&tr[id].r<=r) return tr[id].sum;
		
		push_down(id);
		int mid=(tr[id].l+tr[id].r)>>1,res=0;
		if (mid>=l) res+=query(id<<1,l,r);
		if (mid+1<=r) res+=query(id<<1|1,l,r);
		return res; 
	}
}Tr;

int dep[N],siz[N],dfn[N],tme;
int fa[N],son[N],top[N];
void dfs1(int x,int _dep)
{
	dep[x]=_dep,siz[x]=1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		fa[v]=x;
		dfs1(v,_dep+1);
		siz[x]+=siz[v];
		if (siz[v]>siz[son[x]]) son[x]=v;
	}
}
void dfs2(int x,int _top)
{
	dfn[x]=++tme,top[x]=_top;
	if (!son[x]) return ;
	dfs2(son[x],_top);
	
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==son[x]) continue;
		dfs2(v,v);
	}
}
void _update(int x)
{
	while (x)
	{
		Tr.update(1,dfn[top[x]],dfn[x],1);
		x=fa[top[x]];
	}
}
int _query(int x)
{
	int res=0;
	while (x)
	{
		res+=Tr.query(1,dfn[top[x]],dfn[x]);
		x=fa[top[x]];
	}
	return res;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n;
	for (int i=2,a;i<=n;i++) { cin>>a; tr[++a].push_back(i); }
	
	dfs1(1,1);
	dfs2(1,1);
	Tr.build(1,1,n);
	
	string op;
	int x;
	cin>>q;
	while (q--)
	{
		cin>>op>>x;
		x++;
		if (op=="install") 
		{
			cout<<dep[x]-_query(x)<<"\n";
			_update(x);
		}
		else
		{
			cout<<Tr.query(1,dfn[x],dfn[x]+siz[x]-1)<<"\n";
			Tr.update(1,dfn[x],dfn[x]+siz[x]-1,0);
		}
	}
	return 0;
}

再随机抽取一道树链剖分+线段树

这树链剖分不比打CF有意思?

树上路径维护信息,想到树链剖分。每种颜色(宗教)分别统计外加区修,想到线段树

于是乎,开 n 棵线段树分别维护每种颜色的区间价值最大值以及价值和(显然,这里的区间是 dfn 序区间),然后再用树链剖分维护路径信息就做完啦!

“调试”点:开若干棵线段树就动态开点,然后用数组 rti 记录第 i 棵线段树的根就行,不用开 tr[N][N<<2] ……

然后直接做做完了

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,q;
int w[N],c[N];
int rt[N];
vector <int> tr[N];
struct Segment_Tree
{
	int cnt=0;
	struct node {
		int ls,rs;
		int mx,sum;
	}tr[N<<8];
	void push_up(int id) { tr[id].mx=max(tr[tr[id].ls].mx,tr[tr[id].rs].mx); tr[id].sum=tr[tr[id].ls].sum+tr[tr[id].rs].sum; }
	void update(int &id,int l,int r,int x,int k)
	{
		if (!id) id=++cnt;
		if (l==r) { tr[id].mx=tr[id].sum=k; return ; }
		
		int mid=(l+r)>>1;
		if (mid>=x) update(tr[id].ls,l,mid,x,k);
		else update(tr[id].rs,mid+1,r,x,k);
		push_up(id);
	}
	int query_mx(int &id,int l,int r,int ql,int qr)
	{
		if (l>=ql&&r<=qr) return tr[id].mx;
		
		int mid=(l+r)>>1,res=0;
		if (mid>=ql) res=max(res,query_mx(tr[id].ls,l,mid,ql,qr));
		if (mid+1<=qr) res=max(res,query_mx(tr[id].rs,mid+1,r,ql,qr));
		return res;
	}
	int query_sum(int &id,int l,int r,int ql,int qr)
	{
		if (l>=ql&&r<=qr) return tr[id].sum;
		
		int mid=(l+r)>>1,res=0;
		if (mid>=ql) res+=query_sum(tr[id].ls,l,mid,ql,qr);
		if (mid+1<=qr) res+=query_sum(tr[id].rs,mid+1,r,ql,qr);
		return res;
	}
}Tr;

int fa[N],son[N],siz[N],dep[N];
int dfn[N],tme,top[N];
void dfs1(int x,int _fa)
{
	fa[x]=_fa,siz[x]=1,dep[x]=dep[_fa]+1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		 int v=tr[x][i];
		 if (v==_fa) continue;
		 dfs1(v,x);
		 siz[x]+=siz[v];
		 if (siz[v]>siz[son[x]]) son[x]=v;
	}
}
void dfs2(int x,int _top)
{
	dfn[x]=++tme,top[x]=_top;
	if (!son[x]) return ;
	dfs2(son[x],_top);
	
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==fa[x]||v==son[x]) continue;
		dfs2(v,v);
	}
}
int queryS(int x,int y)
{
	int root=rt[c[x]],res=0;
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		res+=Tr.query_sum(root,1,n,dfn[top[x]],dfn[x]);
		x=fa[top[x]];
	}
	if (dep[x]<dep[y]) swap(x,y);
	res+=Tr.query_sum(root,1,n,dfn[y],dfn[x]);
	return res;
}
int queryM(int x,int y)
{
	int root=rt[c[x]],res=0;
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		res=max(res,Tr.query_mx(root,1,n,dfn[top[x]],dfn[x]));
		x=fa[top[x]];
	}
	if (dep[x]<dep[y]) swap(x,y);
	res=max(res,Tr.query_mx(root,1,n,dfn[y],dfn[x]));
	return res;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>q;
	for (int i=1;i<=n;i++) cin>>w[i]>>c[i];
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	
	dfs1(1,0),dfs2(1,1);
	for (int i=1;i<=n;i++) Tr.update(rt[c[i]],1,n,dfn[i],w[i]);
	string op;
	int x,y;
	while (q--)
	{
		cin>>op>>x>>y;
		if (op=="CC") { Tr.update(rt[c[x]],1,n,dfn[x],0); Tr.update(rt[y],1,n,dfn[x],w[x]); c[x]=y; }
		else if (op=="CW") { Tr.update(rt[c[x]],1,n,dfn[x],y); w[x]=y; }
		else if (op=="QS") cout<<queryS(x,y)<<"\n";
		else cout<<queryM(x,y)<<"\n";
	}
	return 0;
}

最后的树链剖分

轻边重边,不好维护呢。点,好维护nie

所以不如把边的特征在点上表示。为每个点赋值,若一条边两个点的值相同,则是重边,反之则是轻边。这样的话,用线段树记录一下区间左右端点的值,就很好维护了!修改就是附上完全不同的值,加重边就是区间设相同的值

但是在查询的时候要注意小细节(这种细节还是自己推推好啊www)

以及,多测清空……全都清空……不然会出奇奇怪怪的问题……线段树也给我清空啊

调好久!
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int T;
int n,m;
vector <int> tr[N];
struct node{
	int l,r;
	int lc,rc;
	int sum,tag;
	node operator +(const node &y) const{
		node res={0,0,lc,y.rc,sum+y.sum+(rc==y.lc),0};
		return res;
	}
	void clr() { l=r=lc=rc=sum=tag=0; }
};
struct Segment_Tree
{
	node tr[N<<2];
	void push_up(int id)
	{
		tr[id].lc=tr[id<<1].lc,tr[id].rc=tr[id<<1|1].rc;
		tr[id].sum=tr[id<<1].sum+tr[id<<1|1].sum+(tr[id<<1].rc==tr[id<<1|1].lc); 
	}
	void push_down(int id)
	{
		if (!tr[id].tag) return ;
		int ls=id<<1,rs=id<<1|1;
		tr[ls]={tr[ls].l,tr[ls].r,tr[id].tag,tr[id].tag,tr[ls].r-tr[ls].l,tr[id].tag};
		tr[rs]={tr[rs].l,tr[rs].r,tr[id].tag,tr[id].tag,tr[rs].r-tr[rs].l,tr[id].tag};
		tr[id].tag=0;
	}
	void build(int id,int l,int r)
	{
		tr[id].l=l,tr[id].r=r;
		if (l==r)
		{
			tr[id].lc=tr[id].rc=l;
			tr[id].sum=tr[id].tag=0;
			return ;
		}
		
		int mid=(l+r)>>1;
		build(id<<1,l,mid),build(id<<1|1,mid+1,r);
		push_up(id);
	}
	void update(int id,int l,int r,int w)
	{
		if (tr[id].l>=l&&tr[id].r<=r)
		{
			tr[id].lc=tr[id].rc=tr[id].tag=w;
			tr[id].sum=tr[id].r-tr[id].l;
			return ;
		}
		
		push_down(id);
		int mid=(tr[id].l+tr[id].r)>>1;
		if (mid>=l) update(id<<1,l,r,w);
		if (mid+1<=r) update(id<<1|1,l,r,w);
		push_up(id);
	}
	node query(int id,int l,int r)
	{
		if (tr[id].l>=l&&tr[id].r<=r) return tr[id];
		
		push_down(id);
		int mid=(tr[id].l+tr[id].r)>>1;
		if (mid>=l&&mid+1<=r) return query(id<<1,l,r)+query(id<<1|1,l,r);
		else if (mid>=l) return query(id<<1,l,r);
		else if (mid+1<=r) return query(id<<1|1,l,r);
	}
	void clear() { for (int i=0;i<(N<<2);i++) tr[i].clr(); }
}Tr;

int fa[N],son[N],dep[N],siz[N];
int dfn[N],tme,top[N];
void dfs1(int x,int _fa)
{
	fa[x]=_fa,dep[x]=dep[_fa]+1,siz[x]=1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs1(v,x);
		siz[x]+=siz[v];
		if (siz[v]>siz[son[x]]) son[x]=v; 
	}
}
void dfs2(int x,int _top)
{
	dfn[x]=++tme,top[x]=_top;
	if (!son[x]) return ;
	dfs2(son[x],_top);
	
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==fa[x]||v==son[x]) continue;
		dfs2(v,v);
	}
}
void update1(int x,int y,int w)
{
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		Tr.update(1,dfn[top[x]],dfn[x],w);
		x=fa[top[x]];
	}
	if (dep[x]<dep[y]) swap(x,y);
	Tr.update(1,dfn[y],dfn[x],w);
}
int query1(int x,int y)
{
	node res1={0,0,0,0,0,0},res2={0,0,0,0,0,0};
	bool flag=0;
	while (top[x]!=top[y])
	{
		if (dep[top[x]]<dep[top[y]]) swap(x,y),flag^=1;
		if (flag) res2=Tr.query(1,dfn[top[x]],dfn[x])+res2;
		else res1=Tr.query(1,dfn[top[x]],dfn[x])+res1;
		x=fa[top[x]];
	}
	if (dep[x]<dep[y]) swap(x,y),flag^=1;
	if (flag) res2=Tr.query(1,dfn[y],dfn[x])+res2;
	else res1=Tr.query(1,dfn[y],dfn[x])+res1;
	return res2.sum+res1.sum+(res1.lc==res2.lc);
}
void clr()
{
	for (int i=1;i<=n;i++) fa[i]=son[i]=dep[i]=siz[i]=dfn[i]=top[i]=0;
	for (int i=1;i<=n;i++) tr[i].clear();
	tme=0;
	Tr.clear();
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>T;
	while (T--)
	{
		cin>>n>>m;
		for (int i=1,u,v;i<n;i++)
		{
			cin>>u>>v;
			tr[u].push_back(v),tr[v].push_back(u);
		}
		
		dfs1(1,0),dfs2(1,1);
		Tr.build(1,1,n);
		int f=n+1,op,a,b;
		while (m--)
		{
			cin>>op>>a>>b;
			if (op==1) update1(a,b,++f);
			else cout<<query1(a,b)<<"\n";
		}
		
		clr();
	}
	return 0;
}

Day 4

树上启发式合并&线性基

作业链接

是一种离线思想,一般用于解决子树信息维护问题复杂度 O(n log 2n)

1.1 板子

这不能线段树合并吗?

咕了,这是一个优雅的暴力

1.2 啦啦啦

也是一个优雅的暴力。

对于节点 u ,在它的子树中寻找两个节点 v,v ,使得满足 disv+disvdisu×2=k,或是在其子树中寻找一个节点 v 使得 disvdisu=k ,那么一个合法答案就是 depv+depvdepu×2depvdepu ,这若干个值的最小值就是最终答案。

于是乎,用 mapu 的子树中暴力匹配 v,v 。然后剩下的思路就和普通的 dsu on tree 一样了:先跑轻儿子,跑完一个就清空,最后跑重儿子,重儿子不用清

因为是从下往上遍历的这棵树,所以 map 中存的深度应是最浅的深度

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+5;
const int inf=0x3f3f3f3f;
int n,k;
struct node { int nxt,val; };
vector <node> tr[N];
map <ll,int> mp;
int ans=inf;

int dfn[N],tme,id[N],siz[N],son[N];
int dep[N],dis[N];
void dfs1(int x,int _fa)//预处理出各种信息
{
	dep[x]=dep[_fa]+1,siz[x]=1,dfn[x]=++tme,id[tme]=x;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt,w=tr[x][i].val;
		if (v==_fa) continue;
		dis[v]=dis[x]+w;
		dfs1(v,x);
		siz[x]+=siz[v];
		if (siz[v]>siz[son[x]]) son[x]=v; 
	}
}
void update(int x,int k)
{
	if (k==-1) mp[dis[x]]=0;
	else
	{
		if (!mp[dis[x]]) mp[dis[x]]=dep[x];
		else mp[dis[x]]=min(mp[dis[x]],dep[x]);
	}
}
void dfs2(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa||v==son[x]) continue;
		dfs2(v,x);
	}
	if (son[x]) dfs2(son[x],x);
	
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa||v==son[x]) continue;
		for (int j=0;j<siz[v];j++)//要先匹配再加点,防止自己和自己匹配
		{
			int vv=id[dfn[v]+j],ww=k+dis[x]*2-dis[vv];
			if (mp[ww]) ans=min(ans,dep[vv]+mp[ww]-dep[x]*2);
		}
		for (int j=0;j<siz[v];j++) update(id[dfn[v]+j],1);
	}
	update(x,1);
	if (mp[dis[x]+k]) ans=min(ans,mp[dis[x]+k]-dep[x]);
	
	if (x!=son[_fa]) for (int i=0;i<siz[x];i++) update(id[dfn[x]+i],-1);//不是重儿子,自己主动删
}
int main() 
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>k;
	for (int i=1,u,v,w;i<n;i++)
	{
		cin>>u>>v>>w;
		u++,v++;
		tr[u].push_back({v,w}),tr[v].push_back({u,w});
	}
	
	dfs1(1,0),dfs2(1,0);
	if (ans==inf) cout<<-1;
	else cout<<ans;
	return 0;
}

1.3 天天咱又见面了

把一次路径掰成两半:u>lca(u,v),lca(u,v)>v ,然后分别考虑着两个过程的贡献

对于 u>lca(u,v) ,若在点 i 可以观测到 u ,那么满足 depi+wi=depu;对于 lca(u,v)>v ,若点 i 可以观测到 v ,那么满足 depiwi=deplca(u,v)×2depu

所以开两个 map 分别记录两种路径的信息再分别查询就行了。为了不重复统计(在 lca 处)以及不多统计(在路径之外),这里题解非常智慧地提出了一种处理方法:在 s,t 处把两个值都加进去,在 lca 处删去一个值,在 fa[lca] 处删掉另一个值(和树上差分的处理方法同),然后就完美解决这个问题了

警示自己:推式子的时候不要把公共祖先和观测点混为一谈!

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,m,w[N];
vector <int> tr[N];
struct node { int x,op,val; };
vector <node> p[N];
map <int,int> mp1,mp2;
int ans[N];

int siz[N],dep[N],fa[N][25];
int dfn[N],tme,idx[N],son[N];
void dfs1(int x,int _fa)
{
	siz[x]=1,dep[x]=dep[_fa]+1;
	dfn[x]=++tme,idx[tme]=x,fa[x][0]=_fa;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa) continue;
		dfs1(v,x);
		siz[x]+=siz[v];
		if (siz[v]>siz[son[x]]) son[x]=v;
	}
}
inline void init()
{
	for (int j=1;j<=20;j++)
	for (int i=1;i<=n;i++) fa[i][j]=fa[fa[i][j-1]][j-1];
}
inline int lca(int x,int y)
{
	if (dep[x]<dep[y]) swap(x,y);
	int delta=dep[x]-dep[y],lg=0;
	while (delta)
	{
		if (delta&1) x=fa[x][lg];
		delta>>=1,lg++;
	}
	if (x==y) return x;
	
	for (int i=20;i>=0;i--)
	{
		if(fa[x][i]==fa[y][i]) continue;
		x=fa[x][i],y=fa[y][i];
	}
	return fa[x][0];
}
void dfs2(int x,int _fa)
{
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa||v==son[x]) continue;
		dfs2(v,x);
		mp1.clear(),mp2.clear();
	}
	if (son[x]) dfs2(son[x],x);
	
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa||v==son[x]) continue;
		for (int j=0;j<siz[v];j++)
		{
			int vv=idx[dfn[v]+j];
			int size2=p[vv].size();
			for (int k=0;k<size2;k++)
			{
				if (p[vv][k].op==1) mp1[p[vv][k].x]+=p[vv][k].val;
				else mp2[p[vv][k].x]+=p[vv][k].val;
			}
		}
	}
	int size2=p[x].size();
	for (int k=0;k<size2;k++)
	{
		if (p[x][k].op==1) mp1[p[x][k].x]+=p[x][k].val;
		else mp2[p[x][k].x]+=p[x][k].val;
	}
	
	ans[x]=mp1[dep[x]+w[x]]+mp2[dep[x]-w[x]];
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		tr[u].push_back(v),tr[v].push_back(u);
	}
	dfs1(1,0),init();
	for (int i=1;i<=n;i++) cin>>w[i];
	for (int i=1,s,t;i<=m;i++)
	{
		cin>>s>>t;
		int l=lca(s,t);
		p[s].push_back({dep[s],1,1}),p[t].push_back({dep[l]*2-dep[s],2,1});
		p[l].push_back({dep[s],1,-1}),p[fa[l][0]].push_back({dep[l]*2-dep[s],2,-1});
	}
	
	dfs2(1,0);
	for (int i=1;i<=n;i++) cout<<ans[i]<<" ";
	return 0;
}

2.1 线性基

wiki链接

线性基是一种很适合处理异或问题的数据结构(这么像数论的一个东西居然是数据结构……)

若原数组空间为 n ,那么线性基的空间大概是 log n ,且原数组中进行异或运算可表达的所有数都可以在线性基中用异或运算表示出。

用途:可以快速查询若干个数的最大/最小异或和,第k大异或和,或查询一个数是否可以被若干个数异或得出

总之很神奇的一个东西。 pi 记录的是二进制下最高位为第 i 位的可由原数组异或得出的数(不唯一),然后一系列操作

(求排名/给出排名求值待补)

void insert(int x)
{
	for (int i=N;i>=0;i--)
	{
		if (!(x&(1<<i))) continue;
		if (!p[i]) { p[i]=x; return ; }
		x^=p[i];
	}
}
bool check(int x)
{
	for (int i=N;i>=0;i--) if (x&(1<<i)) x^=p[i];
	return x==0;
}
int query_mx()
{
	int res=0;
	for (int i=N;i>=0;i--) res=max(res,rse^p[i]);
	return res;
}
int query_mn()
{
	for (int i=0;i<=N;i++) if (p[i]) return p[i];
	return 0;
}
void rebuild()
{
	for (int i=N;i>=0;i--)
	for (int j=i-1;j>=0;j--) { if (p[i]&(1ll<<j)) p[i]^=p[j]; }
	for (int i=0;i<=N;i++) if (p[i]) d[++cnt]=p[i];	
}
int rank_k(int k)
{
	rebuild();
	if (k>=(1ll<<cnt)) return -1;
	int res=0;
	for (int i=N;i>=0;i--) if (k&(1ll<<i)) res^=d[i];
	return res;
}
int query_rk(int x)
{
	int res=0;
	for (int i=cnt-1;i>=0;i--) if (x>=d[i]) { res+=(1ll<<i); x^=d[i]; }
	return res;
}

单调队列&斜率优化&决策单调性

作业链接

1.单调队列优化

一般和不那么严格的区间选择有关,且区间的左端点是单调的。那么就可以利用题目中的性质用单调队列维护完事儿

根据题目要求判断查询、插入的顺序。一般来讲还是先插入好……

时刻警惕第 0 个状态是否可以刚开始就放


Day 5

网络流

作业链接&wiki链接

?听不懂

?听不懂一点

一、EK增广路算法 O(nm2)

这里的增广路意为从源点到汇点、所有边剩余容量都大于零的一条路径

然后就是暴力 bfs 了。在广搜的过程中,判断边的剩余容量、记录路径上的剩余容量最小值、记录路径(利用一个 pre 数组)。在完成这个过程后,利用记录的路径,给正向边减去容量最小值,给反向边加上容量最小值

为什么要建反向边呢?因为会有以下情况:本来一条容量很大的边可以容纳较大的流量,但因为其容量被很多小流量占用了,它的大容量就只剩一点了……而那些小流量本来能走小容量的边,但占据了大容量,也就导致没能“物尽其用”。然后待补

码(复杂度过不了)
#include <bits/stdc++.h>
#define ll	 long long
using namespace std;
const int N=2e5+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,s,t;
ll dis[2010][2010];
vector <int> e[N];
ll ans;

ll vis[N],pre[N],mn;
inline bool bfs()
{
	for (int i=1;i<=n;i++) vis[i]=0;
	mn=inf;
	queue <int> q;
	q.push(s),vis[s]=1;
	while (!q.empty())
	{
		int u=q.front();
		q.pop();
		
		int _size=e[u].size();
		for (int i=0;i<_size;i++)
		{
			int v=e[u][i];
			if (dis[u][v]>0)
			{
				if (vis[v]) continue;
				mn=min(mn,dis[u][v]);
				pre[v]=u;
				q.push(v);
				vis[v]=1;
				if (v==t) return true;
			}
		}
	} 
	return false;
}
inline void add()
{
	int u=t;
	while (u!=s)
	{
		int v=pre[u];
		dis[v][u]-=mn,dis[u][v]+=mn;
		u=v;
	}
	ans+=mn;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m>>s>>t;
	for (int i=1,u,v,c;i<=m;i++)
	{
		cin>>u>>v>>c;
		e[u].push_back(v),e[v].push_back(u);
		dis[u][v]+=c;
	}
	
	while (bfs()) add();
	cout<<ans;
	return 0;
}

二、Dinic算法 O(n2m)

可恶的EK复杂度浪费我感情

EK 是单路增广, Dinic 是多路增广

EK 中每次 bfs 都只能找到一条增广路,但这复杂度就不好了。在 Dinic 中,用 bfs 给网络分层,即用 dep 记录每个点到源点的距离,那么就可以找到多条最短的增广路了

然后考虑更新。

当前弧优化:若一条路已经被增广过,那么它就不会被增广第二次,可用来剪枝

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=210;
const int M=5e3+2;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,s,t;
int head[N],ans;
struct node { int to,nxt,val; }e[M<<1];
int cnt;

int dep[N],cur[N];
inline void add(int u,int v,int w)//被迫使用链前
{
	e[++cnt].to=v;
	e[cnt].val=w;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
inline bool bfs()//分层
{
	queue <int> q;
	q.push(s);
	for (int i=1;i<=n;i++) dep[i]=inf;
	dep[s]=0;
	cur[s]=head[s];
	while (!q.empty())
	{
		int u=q.front();
		q.pop();
		for (int i=head[u];i;i=e[i].nxt)
		{
			int v=e[i].to;
			if (e[i].val>0&&dep[v]==inf)
			{
				q.push(v);
				cur[v]=head[v],dep[v]=dep[u]+1;
				if (v==t) return true;
			}
		}
	}
	return false;
}
inline int dfs(int x,int sum)
{
	if (x==t) return sum;
	int k,res=0;
	for (int i=cur[x];i&&sum;i=e[i].nxt)
	{
		cur[x]=i;
		int v=e[i].to;
		if (e[i].val>0&&(dep[v]==dep[x]+1))
		{
			k=dfs(v,min(sum,e[i].val));
			if (!k) dep[v]=inf;
			e[i].val-=k,e[i^1].val+=k;
			res+=k,sum-=k;
		}
	}
	return res;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m>>s>>t;
	for (int i=1,u,v,w;i<=m;i++)
	{
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,0);
	}
	
	while (bfs()) ans+=dfs(s,inf);
	cout<<ans;
	return 0;
}

矩阵

矩阵 A={aij}n×m ,表示有 nm

矩阵加法{aij}n×m+{bij}n×m={aij+bij}n×m ,不难观察到这玩意儿有交换律、结合律

矩阵减法:加法的逆运算,对应位置元素相减即可

矩阵乘法: {aij}n×m×{bij}m×p={k=1maik×bkj}n×p ,具有分配律,但不满足交换律(神神奇奇

2.1 矩阵快速幂

和普通的快速幂过程一样,但是在快速幂过程中的乘法是矩阵乘

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=101;
const int MOD=1e9+7;
int n,k;
struct node{
	int a[N][N];
	node() { memset(a,0,sizeof a); }
	inline void build() { for (int i=1;i<=n;i++) a[i][i]=1; }
}a;
node ans;

node operator *(const node &x,const node &y)
{
	node res;
	for (int k=1;k<=n;k++)
	for (int i=1;i<=n;i++)
	for (int j=1;j<=n;j++) res.a[i][j]=(res.a[i][j]+x.a[i][k]*y.a[k][j]%MOD)%MOD;
	return res;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);	
	
	
	cin>>n>>k;
	for (int i=1;i<=n;i++)
	for (int j=1;j<=n;j++) cin>>a.a[i][j];
	
	ans.build();//矩阵快速幂最开始都要这样初始化
	while (k)
	{
		if (k&1) ans=ans*a;
		a=a*a,k>>=1;
	}
	
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=n;j++) cout<<ans.a[i][j]<<" ";
		cout<<"\n";	
	}
	return 0;
}

2.2 斐波那契数列

手推了个寂寞。乖乖去看题解了

不知道为啥,题解想到了用矩阵加速递推速度。对于一个矩阵 [Fi1,Fi2] ,我们希望它乘上一个矩阵 base 使得 [Fi1,Fi2]×base=[Fi,Fi1]

根据定义手推后发现这个 base 数组应是 [1110]。所以就有了一个东西:[F2,F1]×[1110]n2 。新矩阵的第一项就是所求数字

tie码
#inciude <bits/stdc++.h>
#define int long long
using namespace std;
const int MOD=1e9+7;
int n;
struct node 
{
	int a[5][5];
	node() { memset(a,0,sizeof a); }
	void build() { a[1][1]=a[1][2]=a[2][1]=1; a[2][2]=0; }
}a;
node ans;

node operator *(const node &x,const node &y)
{
	node res;
	for (int k=1;k<=2;k++)
	for (int i=1;i<=2;i++)
	for (int j=1;j<=2;j++) res.a[i][j]=(res.a[i][j]+x.a[i][k]*y.a[k][j]%MOD)%MOD;
	return res;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n;
	if (n==1||n==2) { cout<<1; return 0; }
	ans.build();
	a.a[1][1]=a.a[1][2]=1;
	
	n-=2;
	do{
		if (n&1) a=a*ans;
		ans=ans*ans,n>>=1;
	}while (n);
	
	cout<<a.a[1][1];
	return 0;
}
posted @   还是沄沄沄  阅读(37)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示