还是集训

Day 1-基础算法

一、贪心

大概一般是按某种顺序遍历,每次选择价值最大/对后影响最小/限制最大的作为答案

kle.区间选择模型

一般是根据区间的性质,按照左/右端点排序贪心选择;必要时省去删除包含关系的区间

可用调整法证明贪心的正确性:若合法,则一定存在一种最优解选择了它

klee.匹配模型

仍是要给前/后缀排序,然后贪心选择可选的最大/小的数;若是区间匹配或前/后缀带点权,则有序遍历右部点,选择可选的限制最大的点 ( 比如在区间中,限制最大的点就是长度最小的点 )。如果是二维,那就形似扫描线扫描,每次仍是选择限制最大的点

也可用调整法证明正确性

kleee.邻项交换模型

可以先假想出来一个不优的方案,然后交换相邻两项 i,i+1 会使得序列更优,那么一定可以根据题目给出的函数列出针对于 i 的式子,然后就直接根据式子做做完了

e.g.给出若干 01 串,要求将这些 01 串有序拼接后逆序对的数量最少。可以有一个初步想法:0 多的放前面,反之放后面,但这不够准确。记 cnt1i,cnt0i 分别记录 i 串中 0 和 1 的个数,若交换后更优,可以列出式子 cnt0i×cnt1i+1cnt1i×cnt0i+1 ,整理即 cnt0icnt1icnt0i+1cnt1i+1,所以就可以直接根据 cnt0icnt1i 排序

可通过反证法证明贪心正确性

1.1 树上01

最刚开始可以有个思路:能选 0 就选,不选再说,但这课上已有 hack 数据(子树内部)

先考虑一个节点连着若干已确定顺序的子树的模型。这样相当于每个子树就是一个 01 串,要求顺序最优——这和邻向交换模型一毛一样,记个 cnt0,cnt1 然后直接比比完了

哎,然后就可以从一个大问题转为一堆子问题了。为了方便计算,在实现时将子树内部的信息传递至根(形似合并),然后每次合并计算这次合并会产生多少逆序对。于是乎用一个小根堆维护 cnt1icnt0i ,每次让堆顶和其父合并(并查集维护),计算贡献并加入合并后的点。显然 合并后的父节点肯定比原来的父节点先到堆顶,所以用一个 vis 数组记录当前节点是否更新过就行了

#include <bits/stdc++.h>
#define int long long
#define pdi pair<double,int>
#define mkp make_pair
using namespace std;
const int N=2e5+5;
const double inf=1e18;
int n;
int p[N],v[N];
int cnt0[N],cnt1[N];
int fa[N],vis[N],ans;
priority_queue < pdi,vector<pdi>,greater<pdi> > q;

int find_fa(int x) 
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void _merge(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y); 
	ans+=cnt1[fa1]*cnt0[fa2];
	cnt0[fa1]+=cnt0[fa2],cnt1[fa1]+=cnt1[fa2];
	fa[fa2]=fa1;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n;
	for (int i=2;i<=n;i++) cin>>p[i];
	for (int i=1;i<=n;i++)
	{
		cin>>v[i];
		fa[i]=i;
		if (v[i]) cnt1[i]++;
		else cnt0[i]++;
		q.push(mkp((cnt0[i]?(1.0*cnt1[i])/(1.0*cnt0[i]):inf),i));
	}
	
	while (!q.empty())
	{
		int ea=q.top().second;
		q.pop();
		if (vis[ea]||ea==1) continue;
		vis[ea]=1;
		
		int faa=find_fa(p[ea]);
		_merge(faa,ea);
		q.push(mkp((cnt0[faa]?(1.0*cnt1[faa])/(1.0*cnt0[faa]):inf),faa));
	}
	cout<<ans;
	return 0;
}

1.2 带权前缀匹配

这是个带权前缀匹配,这个“前缀”是每个订单可以入住的房间,所以先给房间信息按容纳人数降序排序

然后每个订单的钱是固定的,想要收费最大就要维护费最少(即房间容量最小),所以二分查找一个可以入住的容量最小的房间,这就是相对于该订单最优的方案。

但要是负贡献,要它无用,所以不计

贪心地想,付款多的订单大概能贡献更多,所以把订单按照付款降序排序,然后计算就行

二分错了……不嘻嘻
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
int n,m,o;
struct node { int val,num; }a[N],b[N];
int fa[N];
int ans[N],tol,sum;

bool cmp(int x,int y) { return x>y; }
bool cmp1(node x,node y) { return (x.num!=y.num?x.num>y.num:x.val>y.val); }
bool cmp2(node x,node y) { return (x.val!=y.val?x.val>y.val:x.num<y.num); }
int find_fa(int x) { return (fa[x]==x?x:fa[x]=find_fa(fa[x])); }
int BS(int x)
{
	int l=1,r=n;
	while (l<=r)
	{
		int mid=(l+r)>>1;
		if (a[mid].num>=x) l=mid+1;
		else r=mid-1;
	}
	return find_fa(r);
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m>>o;
	for (int i=1;i<=n;i++) fa[i]=i;
	for (int i=1;i<=n;i++) cin>>a[i].val>>a[i].num;
	for (int i=1;i<=m;i++) cin>>b[i].val>>b[i].num;
	
	sort(a+1,a+1+n,cmp1),sort(b+1,b+1+m,cmp2);
	for (int i=1;i<=m;i++)
	{
		int ea=BS(b[i].num);
		if (!ea||a[ea].val>=b[i].val) continue;
		ans[++tol]=b[i].val-a[ea].val;
		fa[ea]=find_fa(ea-1);
	}
	
	sort(ans+1,ans+tol+1,cmp),o=min(o,tol);
	for (int i=1;i<=o;i++) sum+=ans[i];
	cout<<sum;
	return 0;
}

kleeee.凸性

其实没听

若对于i<j,fjfi 单调不降,那么称 f 是凸函数。显然,开口向上的二次函数是凸函数

(直白讲,就是斜率越来越大)

在某些问题中,要是关注的是关于 k 的最优解且最优解的值关于 k 是凸的,那么会为答案带来很大便利

凸函数卷积:给出两个凸函数 f,g ,求 hi=min{fj+gij}

看着不好求,但可以利用凸函数的性质。凸函数的差值具有单调性,所以可以分别求出两个凸函数的差分数组,此时凸函数卷积就变成了 hi=min{Σk=1jfk+Σk=1ijgk} ,因为单调性,该式可以理解为求两个差分数组中取 i 个最小的数的和

1.2

kleeeee.拟阵

听了没懂,所以待补

wiki链接

二、二分

二分答案往往是需要答案具有单调性,但有的题的二分并不需要单调性,这种往往是通过二分不断判断当前集合子集的性质并缩小范围,最后找到所求元素。神奇的嘞

2.1 Foo Fifhters

三、倍增

如果暴力是一步步,那么可以直接倍增大跨步

3.1

Day 2-动态规划

序列

1.1

首先显然,清空之后万事空。所以找出最后一次必须要清空的

斜率优化

听半天居然都听错了 悲

fi 的转移方程可以用 fi=min{fjai×bj} 的形式表达出,那么在方程移项后可以将其看作经过点 (bj,fj) 、斜率为 ai 、截距为 fi 的一次函数的点斜式,那么就可以将问题转化为求最小截距,可用单调队列维护

首先 显然 并不是所有的转移点 (bj,fj) 都会造成贡献,只有在凸包上的转移点才有用。那么好,记二元组记录转移点的坐标,用单调队列维护相邻转移点的斜率、使得斜率单调递增,那么队列中的点就是会贡献的转移点了。

斜率优化DP

设状态 fi 表示以 i 为容器结束点最小的总费用。那么转移要从容器的断点 j 处转移

fi=min{fj+(sumisumj1L)2},sumi=i+Σj=1icj

为了转移方便,开局给 L 加一,再给转移方程变形后得

2×(sumiL)×sumj+fi(sumiL)2=fj+sumj2

那么可以将其斜率看作为 2×(sumiL) ,经过点 (sumj,fj+sumj2) 的直线

容易发现 在这道题中的斜率和横坐标都是单调不降的,那么直接用单调队列维护就好

板?
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
int n,L;
int sum[N],f[N];
int q[N],hd,tl;

int x(int i) { return sum[i]; }
int y(int i) { return f[i]+sum[i]*sum[i]; }
double kk(int i,int j) { return 1.0*(y(j)-y(i))/(1.0*x(j)-x(i)); }
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>L,L++;
	for (int i=1;i<=n;i++) { cin>>sum[i]; sum[i]+=sum[i-1]+1; }

	for (int i=1;i<=n;i++)
	{
		while (hd<tl&&kk(q[hd],q[hd+1])<=(sum[i]-L)*2) hd++;//根据斜率找到j 
		f[i]=f[q[hd]]+(sum[i]-sum[q[hd]]-L)*(sum[i]-sum[q[hd]]-L);
		while (hd<tl&&kk(q[tl-1],q[tl])>=kk(q[tl-1],i)) tl--;//一定要这么写… 
		q[++tl]=i;
	}
	cout<<f[n];
	return 0;
}

决策单调性优化

fifj 转移, j 单调递增,那么可优化

决策单调性优化DP

设状态 fi 表示以 i 结尾的最小代价,那么可列出形如上题的状态转移方程(仍是开局给 L 加一)

fi=min{fj+|sumisumjL|P}

经过打表后 发现决策最优点具有单调性

但这里不能直接用双指针,因为不保证它不是曲里拐弯的

什么是决策单调性呢。可以画出一个矩阵,fi,j 表示 dpidpj 转移来的值,而每一行的最小值所在的列是单调递增的

豪德,每当更新了一个 fi ,就二分去找它以后的哪些后缀用它更新会更优

道理我都懂关键是程序实现啊 用一个单调队列维护那些个最优转移点,再记 ki 表示由第 i 个转移点的最后一个位置+1(即用下一个转移点转移的第一个位置)。在二分时,要把完全覆盖掉的那些转移点都 pop 掉,再修改最后的 k

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int T;
int n,l,p;
int sum[N],pre[N];
long double f[N];
int k[N],q[N],hd,tl;
string str[N];

inline long double qsm(long double x,int y)
{
	long double res=1;
	if (x<0) x=-x;
	while (y)
	{
		if (y&1) res*=x;
		x*=x,y>>=1;
	}
	return res;
}
inline long double calc(int j,int i) { return f[j]+qsm(sum[i]-sum[j]-l,p); }
inline int BS(int x,int y)
{
	int l=x,r=n+1;
	while (l<r)
	{
		int mid=(l+r)>>1;
		if (calc(y,mid)<=calc(x,mid)) r=mid;
		else l=mid+1;
	}
	return r;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>T;
	while (T--)
	{
		cin>>n>>l>>p,l++;
		for (int i=1;i<=n;i++) { cin>>str[i]; sum[i]=sum[i-1]+str[i].size()+1; }
		
		hd=tl=1,q[1]=0;
		for (int i=1;i<=n;i++)
		{
			while (hd<tl&&k[hd]<=i) hd++;//将不覆盖i的前缀pop掉
			f[i]=calc(q[hd],i),pre[i]=q[hd];
			while (hd<tl&&k[tl-1]>=BS(q[tl],i)) tl--;//将被完全覆盖的转移点 pop 掉
			k[tl]=BS(q[tl],i),q[++tl]=i;
		}
		
		if (f[n]>1e18) cout<<"Too hard too arrange\n";
		else//史一般的输出
		{
			cout<<(int)(f[n]+0.5)<<"\n";
			tl=0,q[0]=n;
			for (int i=n;i;i=pre[i]) q[++tl]=pre[i];
			int idx=1;
			for (int i=tl;i;i--)
			{
				while (idx<q[i-1]) cout<<str[idx++]<<" ";
				cout<<str[idx++]<<"\n";
			}
		}
		cout<<"--------------------";
		if (T) cout<<"\n";
	}
	return 0;
} 

Day 3

模拟赛

补题链接

满脑子都是主席树……

T1 手模几下后模出来了。二阶前缀和、式子都没问题,但没有特判外加细节处理不到位(没有给所有值的 set 加上边界),然后正解直接挂成暴力……唉

警示自己
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,m,a[N];
set <int> st[N];
struct BIT
{
	int tr1[N],tr2[N];
	int lowbit(int x) { return x&-x; }
	void add(int x,int y)
	{
		if (!x) return ;
		int yy=(1-x)*y;
		while (x<N) { tr1[x]+=y; tr2[x]+=yy; x+=lowbit(x); }
	}
	int sum(int x)
	{
		int res1=0,res2=0,k=x;
		while (x) { res1+=tr1[x]; res2+=tr2[x]; x-=lowbit(x); }
		return res1*k+res2;
	}
}bt;

signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1;i<=n;i++) st[i].insert(0),st[i].insert(n+1);
	for (int i=1;i<=n;i++)
	{
		cin>>a[i];
		auto id=st[a[i]].upper_bound(i),id--;
		bt.add(i,i-(*id));
		st[a[i]].insert(i);
	}
	
	int op,x,y,k;
	while (m--)
	{
		cin>>op;
		if (op==1)
		{
			cin>>x>>y;
			if (y==a[x]) continue;
			
			int pre,lst,res1,res2;
			auto i=st[a[x]].upper_bound(x);
			lst=*i,i--,i--,pre=*i;
			i=st[y].upper_bound(x);
			res2=*i,i--,res1=*i;
			
			bt.add(lst,x-pre);
			bt.add(x,pre-res1);
			bt.add(res2,res1-x);
			
			st[a[x]].erase(x),st[y].insert(x);
			a[x]=y;
		}
		else { cin>>k; cout<<bt.sum(k)<<"\n"; }
	}
	return 0;
}

T2不会,写了个暴力16分。但不知道为啥全挂没了

T3想到了不带修的做法,但是没想到修改怎么维护。最后只打了个 8 分暴力。现在看来,这道题是属于正解和部分分半点关系都没有的题……

总结:推出 T1 式子的时候感觉自己有救了,但先是像上次一样忘记了 map 的存在、没有用更方便的 set 而是手写了个主席树,然后是式子推对结果上代码写反了(其实是因为那时觉得式子推错了……但其实没错)。还是码力不足,平时口胡或先看题解再写就会导致自己写的时候会漏掉一些特殊情况;以及还是一直以来的问题:细节处理不到位,看边界看边界看边界。我要练就超级码力!!!

以及,总是需要调很长时间的代码。不管是不小心敲错导致的错误,还是逻辑漏洞,有时都需要调很久。还是码力不足啊啊啊。下次一定自己主动调程序

现在的模拟赛时间分配不能像学期内那时候那样了。现在大概有能力切题,还是得把暴力(说到这,立个flag:下次调暴力时间不超过15分钟)快速打完后集中注意力去切。写完代码后也要立刻去调,不然下次再正解挂成暴力就老实了……

另外,自信一些。有些想法是对的


树的直径

可以跑两遍 dfs (不适用于负边权)或换根 dp

可合并性:两个点集合并后的直径端点一定在合并前各自直径的4个端点之中->可以用线段树维护(区间内的点构成的直径)

1.1

树的重心

以树的重心为根时,所有子树的大小不超过 n2,所有点到重心的距离和最小,一棵树最多有两个重心(若是两个则重心相邻),重心是最深的满足 szxn2的点(可用数据结构维护)

有些时候以重心为根会有很好的性质

DFN序

在求所有点和给定点集中的点的最浅 LCA 时,可以只比较给出子集中 dfn 序最大的和最小的点求出 LCA

欧拉序

可以 O(1) 求 LCA

重链剖分

好东西

一个点到根经过的链的数量是 log n

证明:向上每经过一个轻边,子树大小至少翻倍

1.2

首先,根据调整法发现边权不重要

1.3 深度转权值后树剖维护

有个小 trick: 可以把 deplca(u,v) 掰成给 u 到根的路径 +1,查询 v 到根的权值。这样的话,就可以 log 查询了。仍是给 l,r 的查询掰成 ansransl1 ,然后直接跑出来所有的 ansi ,将查询离线后算出最后的答案就好了

小声:好久没遇到这么友好的代码了www

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
const int MOD=201314;
int n,m;
vector <int> tr[N];
struct node { int val,k,id; };
vector <node> q[N];
int ans[N];
struct Segment_Tree
{
	struct node 
	{
		int l,r;
		int sum,tag;
	}tr[N<<2];
	void push_up(int id) { tr[id].sum=(tr[id<<1].sum+tr[id<<1|1].sum)%MOD; }
	void push_down(int id)
	{
		if (!tr[id].tag) return ;
		int ls=id<<1,rs=id<<1|1;
		tr[ls].sum=(tr[ls].sum+(tr[ls].r-tr[ls].l+1)*tr[id].tag%MOD)%MOD;
		tr[rs].sum=(tr[rs].sum+(tr[rs].r-tr[rs].l+1)*tr[id].tag%MOD)%MOD;
		tr[ls].tag+=tr[id].tag,tr[rs].tag+=tr[id].tag;
		tr[ls].tag%=MOD,tr[rs].tag%=MOD;
		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)
	{
		if (tr[id].l>=l&&tr[id].r<=r) { tr[id].sum=(tr[id].sum+tr[id].r-tr[id].l+1)%MOD; tr[id].tag++; return ; }
		
		push_down(id);
		int mid=(tr[id].l+tr[id].r)>>1;
		if (mid>=l) update(id<<1,l,r);
		if (mid+1<=r) update(id<<1|1,l,r);
		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 res=0,mid=(tr[id].l+tr[id].r)>>1;
		if (mid>=l) res+=query(id<<1,l,r);
		if (mid+1<=r) res+=query(id<<1|1,l,r);
		return res%MOD;
	}
}Tr;

int fa[N],siz[N],dep[N],son[N];
int dfn[N],tme,top[N];
void dfs1(int x,int _fa)
{
	siz[x]=1,fa[x]=_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;
		dfs1(v,x);
		siz[x]+=siz[v];
		if (siz[son[x]]<siz[v]) son[x]=v;
	}
}
void dfs2(int x,int _top)
{
	top[x]=_top,dfn[x]=++tme;
	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 update(int x)
{
	while (top[x])
	{
		Tr.update(1,dfn[top[x]],dfn[x]);
		x=fa[top[x]];
	}
}
void _query(int id,int val,int x)
{
	while (top[x])
	{
		ans[id]=(ans[id]+val*Tr.query(1,dfn[top[x]],dfn[x])+MOD)%MOD;
		x=fa[top[x]];
	}
}
void solve()
{
	for (int i=1;i<=n;i++)
	{
		update(i);
		int _size=q[i].size();
		for (int j=0;j<_size;j++) _query(q[i][j].id,q[i][j].val,q[i][j].k);
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1,fa;i<n;i++) { cin>>fa; tr[fa+1].push_back(i+1); }
	
	dfs1(1,0),dfs2(1,1);
	Tr.build(1,1,n);
	int l,r,x;
	for (int i=1;i<=m;i++)
	{
		cin>>l>>r>>x,l++,r++,x++;
		q[l-1].push_back({-1,x,i});
		q[r].push_back({1,x,i});
	}
	
	solve();
	for (int i=1;i<=m;i++) cout<<(MOD+ans[i])%MOD<<"\n";
	return 0;
}

长链剖分

子树深度最大的子节点是长链

一个点到根的路径经过的轻边数量是 O(n)

长链剖分可以优化与深度有关的 DP

树上启发式合并

维护子树信息好评

Prufer 序列

有标号无根

每次删除编号最小的叶子结点并将其父节点编号加入序列末尾

点的度数等于其在序列中出现的次数+1

n 个点无根树数量是 nn2

? what admire 这都是些啥啊

点分治

wiki链接

感觉其思想和 dsu on tree 很像,都是在 LCA 统一处理

但这个大概是从上到下治……从重心开始处理,处理完后到它的子树里的重心继续处理。因为每次取的都是重心,它的子树们的大小不会超过 n2 ,所以一共只需要 log 次分治

点分治板

板码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m;
struct node { int nxt,val; };
vector <node> tr[N];
int q[N],ans[N],rt;
int a[N],dis[N],b[N],tol;

bool cmp(int x,int y) { return dis[x]<dis[y]; }
int siz[N],mx[N],vis[N];
void get_rt(int x,int _fa,int tol)//找重心 
{
	siz[x]=1,mx[x]=0;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa||vis[v]) continue;
		get_rt(v,x,tol);
		siz[x]+=siz[v];
		mx[x]=max(mx[x],siz[v]);
	}
	mx[x]=max(mx[x],tol-siz[x]);
	if (!rt||mx[x]<mx[rt]) rt=x;
}
void get_dis(int x,int _fa,int rrt)
{
	a[++tol]=x;
	b[x]=rrt;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa||vis[v]) continue;
		dis[v]=dis[x]+tr[x][i].val;
		get_dis(v,x,rrt);
	}
}
void calc(int x)
{
	tol=0;
	a[++tol]=x;
	dis[x]=0,b[x]=x;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)//计算子树内所有点到 x 的位置 
	{
		int v=tr[x][i].nxt;
		if (vis[v]) continue;
		dis[v]=tr[x][i].val;
		get_dis(v,x,v);
	}
	
	sort(a+1,a+1+tol,cmp);
	for (int i=1;i<=m;i++)
	{
		int l=1,r=tol;
		while (l<r)//双指针 
		{
			if (dis[a[l]]+dis[a[r]]>q[i]) r--;
			else if (dis[a[l]]+dis[a[r]]<q[i]) l++;
			else if (b[a[l]]==b[a[r]]) dis[a[r]]==dis[a[r-1]]?r--:l++;//需在不同的子树中 
			else { ans[i]=1; break; }
		}
	}
}
void solve(int x)
{
	vis[x]=1,calc(x);
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)//处理它的子树们 
	{
		int v=tr[x][i].nxt;
		if (vis[v]) continue;
		rt=0;
		get_rt(v,0,siz[v]);
		solve(rt);
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1,u,v,w;i<n;i++)
	{
		cin>>u>>v>>w;
		tr[u].push_back({v,w}),tr[v].push_back({u,w});
	}
	for (int i=1;i<=m;i++) cin>>q[i];
	
	mx[0]=n;
	get_rt(1,0,n);
	solve(rt);
	for (int i=1;i<=m;i++) cout<<(ans[i]?"AYE\n":"NAY\n");
	return 0;
}

树哈希

wiki链接

哈希函数 f(x)=1+vsonr(f(v)) ,当然这并不唯一,也可以用其他合理的哈希函数(但小心被卡)

1.3 树同构

虚树

wiki链接

1.4 虚树

点分树

这种强制在线还带修的就不好跑普通的点分治了。于是出现了点分树

点分治每次沿着子树的重心分治,而点分树就是根据遍历的重心顺序建成的树。也就是说,点分树上一个点的子节点就是在原树上计算完该点后要处理的下若干个子树的重心

点分树有两个性质:

  1. 因为点分治的递归深度不会超过 log n,所以点分树的深度是 log 级别的
  2. 在点分树上的 lca(u,v) 一定在原树 u,v 的路径上,反之不一定

现在考虑怎么用点分树维护信息。设震中 x 可以影响到点 y ,那么考虑枚举 x,y 在点分树上的 lca:z (显然这枚举最多 log 次),那么求出所有满足 dis(y,z)=kdis(z,x)y 的权值和即可,可用线段树维护。

和 dsu on tree 一样,统计权值和时不能统计 yx 同子树时的贡献。怎么办呢,直接扣掉 x 方向子树的贡献就行了

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m;
int val[N];
vector <int> tr[N];
int rt,dfa[N]; 
int rt1[N],rt2[N],ans;
struct Segment_Tree
{
	struct node{
		int ls,rs;
		int sum;
	}tr[N<<6];
	int cnt=0;
	void push_up(int id) { tr[id].sum=tr[tr[id].ls].sum+tr[tr[id].rs].sum; }
	void update(int &id,int l,int r,int pos,int k)
	{
		if (!id) id=++cnt;
		if (l==pos&&r==pos) { tr[id].sum+=k; return ; }
		
		int mid=(l+r)>>1;
		if (pos<=mid) update(tr[id].ls,l,mid,pos,k);
		else update(tr[id].rs,mid+1,r,pos,k);
		push_up(id);
	}
	int query(int id,int l,int r,int ql,int qr)
	{
		if (!id) return 0;
		if (l>=ql&&r<=qr) return tr[id].sum;
		
		int mid=(l+r)>>1,res=0;
		if (mid>=ql) res+=query(tr[id].ls,l,mid,ql,qr);
		if (mid+1<=qr) res+=query(tr[id].rs,mid+1,r,ql,qr);
		return res;
	}
}Tr1,Tr2;

int fa[N][20],dep[N];
void dfs(int x,int _fa)
{
	fa[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);
	}
}
inline void init()
{
	for (int j=1;j<=18;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=18;i>=0;i--)
	{
		if (fa[x][i]==fa[y][i]) continue;
		x=fa[x][i],y=fa[y][i];
	}
	return fa[x][0];
}
int mx[N],siz[N],vis[N];
void get_rt(int x,int _fa,int tol)
{
	siz[x]=1,mx[x]=0;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (v==_fa||vis[v]) continue;
		get_rt(v,x,tol);
		siz[x]+=siz[v];
		mx[x]=max(mx[x],siz[v]);
	}
	mx[x]=max(mx[x],tol-siz[x]);
	if (!rt||mx[x]<mx[rt]) rt=x;
}
void div_rt(int x,int tol)
{
	vis[x]=1;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i];
		if (vis[v]) continue;
		rt=0,get_rt(v,0,(siz[v]<siz[x]?siz[v]:tol-siz[x]));
		dfa[rt]=x,div_rt(rt,(siz[v]<siz[x]?siz[v]:tol-siz[x]));
	}
}
inline int dis(int x,int y) { return dep[x]+dep[y]-2*dep[lca(x,y)]; }
inline void update(int x,int k)
{
	int tmp=x;
	while (tmp)
	{
		Tr1.update(rt1[tmp],0,n-1,dis(tmp,x),k);
		//为什么一定要麻烦巴拉地建两棵线段树?因为点分树上的相邻点可能相差十万八千里,不能直接在第一棵线段树上抠掉 [0,dis(tmp,x)-1]的数值因为你根本不知道他们相差是否为1相差多少…… 
		if (dfa[tmp]) Tr2.update(rt2[tmp],0,n-1,dis(dfa[tmp],x),k);
		tmp=dfa[tmp];
	}
}
inline int query(int x,int y)
{
	int tmp=x,pre=0,res=0;
	while (tmp)
	{
		if (dis(tmp,x)>y) { pre=tmp; tmp=dfa[tmp]; continue ; }
		res+=Tr1.query(rt1[tmp],0,n-1,0,y-dis(tmp,x));
		if (pre) res-=Tr2.query(rt2[pre],0,n-1,0,y-dis(tmp,x));//扣掉x方向的 
		pre=tmp,tmp=dfa[tmp]; 
	}
	return res;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1;i<=n;i++) cin>>val[i];
	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),init();//处理LCA相关 
	get_rt(1,0,n),div_rt(rt,n);//建点分树 
	for (int i=1;i<=n;i++) update(i,val[i]);
	
	int op,x,y;
	while (m--)
	{
		cin>>op>>x>>y;
		x^=ans,y^=ans;
		if (!op) { ans=query(x,y); cout<<ans<<"\n"; }
		else { update(x,y-val[x]); val[x]=y; }
	}
	return 0;
}

脸洗啼

《点分治好题》

本来想的是 dsu on tree ,但是这只能处理子树内的,没法求,但是淀粉质可以处理所有子树

另类点分治:若能确定更优点在某个子树内则分治递归

在这题中,显然 当贡献点对 (x,y) 在 P 的不同子树内时,就没法使得这个点对更优了;反之,则可能“可以”更优,那么到这棵子树中找重心后继续治

调题过程

模拟赛摆烂后大概调了……我也不知道多长时间。大概是一些记录的点的顺序有问题

然后因为没细想,没“剪枝”,导致 T 成了50.调调调,调调调,卡常无果,去看题解

发现当且仅当只有一棵子树存在以上情况的时候需要到这棵子树内,不然倒来倒去总有一些点对会不优

然后剪完就过了 下次一定推到底

#include <bits/stdc++.h>
#define pii pair<int,int>
#define mkp make_pair
#define fst first
#define scd second
using namespace std;
const int N=1e5+5;
const int inf=0x3f3f3f3f;
int n,m;
struct node { int nxt,val; };
vector <node> tr[N];
pii q[N];
int rt,ans=inf;
int mx[N],siz[N];

int b[N],dis[N],vis[N];
void get_rt(int x,int _fa,int tol)
{
	siz[x]=1,mx[x]=0;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa||vis[v]) continue;
		get_rt(v,x,tol);
		siz[x]+=siz[v];
		mx[x]=max(mx[x],siz[v]);
	}
	
	mx[x]=max(mx[x],tol-siz[x]);
	if (!rt||mx[x]<mx[rt]) rt=x;
}
void get_dis(int x,int _fa,int fm)
{
	b[x]=fm;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		if (v==_fa) continue;
		dis[v]=dis[x]+tr[x][i].val;
		get_dis(v,x,fm);
	}
}
int calc(int x)
{
	dis[x]=0;
	int _size=tr[x].size();
	for (int i=0;i<_size;i++)
	{
		int v=tr[x][i].nxt;
		dis[v]=tr[x][i].val;
		get_dis(v,x,v);
	}
	
	int res=0,flag=0,lst=0,cnt=0;
	for (int i=1;i<=m;i++)
	{
		int sum=dis[q[i].fst]+dis[q[i].scd];
		if (sum>res) { res=sum; cnt=0; }
		if (sum==res) b[q[i].fst]==b[q[i].scd]?lst=b[q[i].fst],cnt++:flag=sum; 
	}
	
	ans=min(ans,res);
	if (flag==res||cnt>1) return 0;
	return lst;
}
void solve(int x)
{
	vis[x]=1;
	int c=calc(x);
	if (!c||vis[c]) return ;
	rt=0,get_rt(c,0,siz[c]);
	solve(rt);
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	
	cin>>n>>m;
	for (int i=1,u,v,w;i<n;i++)
	{
		cin>>u>>v>>w;
		tr[u].push_back({v,w}),tr[v].push_back({u,w});
	}
	for (int i=1;i<=m;i++) cin>>q[i].fst>>q[i].scd;
	
	get_rt(1,0,n),solve(rt);
	cout<<ans;
	return 0;
}

Day-5

再考直接爆炸

T1 非常神奇。对于 (a,b),(c,d) 虽然不知道 a,c 之外的路径是啥,但是可以确定肯定会经过 a+c2 的那条边。因为 m 非常小,所以可以直接枚举这条边所在的行,然后线段树一直做下去

T2 难绷

T3 没想到真的和网络流有关……听不懂听不懂

线段树

线段树二分

?好腼腆的老师

区间最小未出现的自然数

显然主席树。但若是常规的“计数”,非常地不好处理。可以联想 HH 的项链中数的贡献方式:在 r 的树中,若某数 x 最后的位置小于 l ,那么它就不在区间内。所以主席树维护每个数最后出现的位置,查询直接分分完了


Day 6

不是 到底是怎么做到一分不剩全都挂掉的???

破案了,暴力数组开小了

posted @   还是沄沄沄  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示