心路历程

心路历程

写在前面

文字是感受的理性化,把自己做题时候的想法写出来,既可以理清思路,又能记录当时的想法,方便以后回忆

标识:@水题 ~未做完 *好题

1.贪吃的九头龙

明显的树形DP

m=2的时候,设置状态dp[x][t][1/0]表示x子树中已有t个被吃,x是否被大头吃

则有

\[dp[x][t][1]+=min(dp[y][t-1][0],dp[y][t][1]+val); dp[x][t][0]+=min(dp[y][t][0]+val,dp[y][t][1]); \]

最后取dp[rt][m][0/1]作为答案

当m>2时,则每条非大头吃的树枝都可以被避免,则有

\[dp[x][t][1]+=min(dp[y][t-1][0],dp[y][t-1][1]+val); dp[x][t][0]+=min(dp[y][t][0],dp[y][t][1]); \]

初始值,都为0

很显然,不合理

显然还需要枚举子树已吃了多少个

2.巡逻

k=1时候很明显是求树的直径

k=2的时候只需要把树的直径求出来之后,把直径标成-1,再求一边树的直径

如果在考场上k=1还是能想出来的,k=2的时候,就要充分发挥算法的简便,即不要什么都想自己算出来

3.通往自由的钥匙 @

很显然的树上背包

一开始看错数据范围,以为不可做

注意的点就是每扇门去了就需要消耗能量

则可以设置状态为

F[x][k]表示以x为根的子树中已经消耗了k能量能获得的最大钥匙数量

则有

\[F[x][k]=max(F[x][k],F[y][t]+F[x][k-t]) \]

注意都要反向枚举

4.选课 @

树形背包模板,觉得不太熟悉这个模型就来练一练

很明显需要抽象建图,若选课b需要先学a,那么a就是b的父亲

设置状态F[x][k]表示到以x为根的子树已选择了k个能得到的最大学分

初始状态则有

对于每个节点,F[x][0]=0,F[x][1]=s[x];

之后,转移方程为

\[F[x][j]=max(F[x][j],F[x][j-t]+F[y][t]) \]

然后考虑枚举顺序

很显然,如果正向枚举j会导致调用到刚刚更新过的状态,所以反向枚举j

5.医院建设 @

带权树重心

树的重心的性质如下:

1.将树分割成两个size不超过原先1/2的树

2.所有点到重心的距离和最小

设置状态

F[x]表示以x为根的树的总距离为多少

那么就有

F[y]=F[x]-sz[y]+sz[1]-sz[y]

6.T168872 【T3】既见君子 ~

路径统计

把一个联通图删成树之后,求1~n的简单路径必须经过z的概率

对于一个联通图

貌似需要矩阵树定理,不会,做不了

7.树的双中心 *

如果知道树的重心的话,这道题就很想了

题目名称就告诉我们,是要把一棵树分成两个部分,再求两个重心

如果这样的话,就可以暴力断边,再求重心

总的时间复杂度为n方,显然过不了

考虑优化,

我们考虑到转移方程为

\[F[y]=F[x]+sz[1]-2*sz[y]; \]

则可以推断出,当sz[1]<2*sz[y]时,会更优

同时,加/删点会可能使最大儿子变化,所以维护次大儿子即可

具体代码实现

void getans(int x,int now,int all,int &res)
{
    res=min(res,now);
    int y=son1[x];
    if(sz[y]<sz[son2[x]]||y==cur)
        y=son2[x];
    if(sz[y]*2>sz[x]&&y)
        getans(y,now+all-2*sz[y],all,res);
}
void find(int x)
{
	for(int i=head[x];i;i=nxt[i])
    {
        int y=to[i];
        if(y==fa[x])
            continue;
        cur=y;
        for(int j=x;j;j=fa[j])
            sz[j]-=sz[y];
        int a,b;
        a=inf,b=inf
            getans(1,f[1]-f[y]-(dep[y]-dep[1])*sz[y],sz[1],a);
        	getans(y,f[y],sz[y],b);
        ans=min(ans,a+b);
        for(int i=x;i;i=fa[i])
            sz[i]+=sz[y];
        find(y);
    }
}

最大值设大!!!!

8.动态逆序对

先考虑朴素算法

建一棵权值线段树,对线段树进行删除操作,然后\(nlog\)求逆序对,总体复杂度为\(mnlogn\)

考虑优化,很明显需要考虑每个数字被删除之后对总体的贡献

当前逆序对有\(t\)个,我们删除了\(i\)位置上的\(a_i\)

他的贡献就是\(\Sigma_{j=1}^i[a_j>a_i]+\Sigma_{j=i+1}^n[a_j<a_i]\)

利用主席树就可以在有编号限制的同时维护权值线段树

考虑如何删除

树状数组套主席树

9.Dynamic Rankings ~

带单点修改的区间第k大查询

看到区间第k大查询,首先想到主席树,前缀建i个树,建成一个大的主席树

而如果要单点修改,那么就需要对该点之后的所有版本的主席树进行修改,妥妥T飞

对于原本的序列,我们建一棵线段树维护,对于线段树上每个点所代表的区间,我们建一棵权值线段树,考虑到每个权值线段树由两个儿子合并而来,所以考虑线段树合并

总的时间复杂度约为\(nlog^3n\)

过不了

假如修改i点,我们就需要对i~n所有版本的主席树进行修改,所以我们可以套一个线段树

给线段树打上lazy标记,l~r表示版本l到r都需要删除/增加某个数,查询的时候往下推即可

貌似能过,试一试

10.聪聪可可 @

点分治裸题,甚至比模板简单一点,需要注意点分治算法本身是把自己到自己算进去的

11.Race*

点分治找出权值和等于k的,全局记一下边数最少的

考虑用桶来做,对于根节点的子树,每处理完一个之后再将其扔进桶里,处理时,因为前面的桶都不是这颗子树的,所以不用容斥

考虑都需要维护什么,可以\(t[i]=x\)表示到根的距离为i的点,最少的边数

折腾三个小时,memset卡时间

12.最短路径树问题*

纯纯sb题

SPFA套个点分治,随便做,拉个数学竞赛的过来都会做

13.重建计划 *

感觉还是很可做的

这个表达式很容易想到0/1分数规划

所以二分答案,然后找\([L,R]\)长度的最大路径

很显然需要点分治

因为多次点分治,所以需要记录下来重心的顺序

对于每次点分治,可以用一个桶T[i]表示深度为i的最大边权和。之后我们对于当前的节点,设为\(\{x,dep,dis\}\),我们需要找的是\(max(T[k](k \in[L-k,R-k]))\) ,也就是区间最大值,自然而然想到线段树,但是这样又会加一个log,肯定会T。

我们发现,k是单调递增的,且k的区间长度不会变,也就是说这是个滑动窗口。

所以最后就是二分+点分治+滑动窗口

还是道很好的题,值得再做一次

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double db;
const int N=2e5+7,inf=1e9+7;
const db INF=1e18+7;
const db eps=1e-5;
int head[N],nxt[N<<1],to[N<<1],val[N<<1],tot;
void add(int x,int y,int z){to[++tot]=y;val[tot]=z;nxt[tot]=head[x];head[x]=tot;}
int n,L,R;//
int RT,rt,sz[N],mx[N],tsz;//根
bool vis[N];
void find_rt(int x,int fa)
{
	sz[x]=mx[x]=1;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(y==fa||vis[y]) continue;
		find_rt(y,x);
		sz[x]+=sz[y];
		mx[x]=max(mx[x],sz[y]);
	}
	mx[x]=max(mx[x],tsz-sz[x]);
	if(mx[x]<mx[rt])rt=x;
}
vector<int> V[N];//因为需要多次点分支,且每次点分支时重心是不变的,所以搞一颗点分树
void build(int x)
{
	vis[x]=1;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(vis[y]) continue;
		rt=0;
		tsz=sz[y];
		find_rt(y,x);
		V[x].push_back(rt);
		build(rt);
	}
	vis[x]=0;
}
db tmp[N],mid,T[N];//当前子树的桶 二分答案 整体的桶
int tp,Tp,Q[N];//某个子树最大深度,所有子树最大深度,单调队列的编号
bool flag;
void dfs(int x,int fa,int dep,db dis)
{
	tp=max(tp,dep);
	tmp[dep]=max(tmp[dep],dis);
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(y==fa||vis[y]) continue;
		dfs(y,x,dep+1,dis+val[i]-mid);
	}
}
//对于每个桶T 下标表示距离根节点的距离 值表示最小值
void work(int x)
{
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(vis[y]) continue;
		dfs(y,x,1,val[i]-mid);
		int l=1,r=0;
		for(int ld=Tp,rd=1;rd<=tp;rd++) // rd 枚举当前子树的深度 ld表示下一个取T里的元素的位置
		{
			while(l<=r&&Q[l]>R-rd) l++; //如果当前子树深度和单调队列队头元素相加大于R 则弹出
			while(ld>=L-rd&&ld>=0)//
			{
				while(l<=r&&T[Q[r]]<=T[ld]) r--; // 去掉废物 把ld加到单调队列里
				Q[++r]=ld;ld--;//加完后ld左移
			}
			if(l<=r&&T[Q[l]]+tmp[rd]>0) {flag=1;break;}
			
		}
		for(int j=1;j<=tp;j++) T[j]=max(T[j],tmp[j]); //更新桶 最大DEP
		Tp=max(Tp,tp);
		for(int j=1;j<=tp;j++) tmp[j]=-INF;tp=0; //复原临时桶
		if(flag) break;//如果有符合条件 break
	}
	for(int i=1;i<=Tp;i++) T[i]=-INF;Tp=0; //记得清空T
}
void solve(int x)
{
	vis[x]=1;
	work(x);
	if(flag){vis[x]=0;return;}
	int len=V[x].size();
	for(int i=0;i<len;i++)
	{
	solve(V[x][i]);
	if(flag) break;
	}
	vis[x]=0;
}
bool check()
{
	flag=0;
	solve(RT);
	return flag;
}
int main()
{
	mx[0]=inf;
	scanf("%d%d%d",&n,&L,&R);
	for(int i=1;i<=n;i++) T[i]=tmp[i]=-INF;
	int a,b,c;
	db l=inf,r=0,ans=2333;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a,&b,&c);
		l=min(l,1.0*c),r=max(r,1.0*c);
		add(a,b,c);
		add(b,a,c);
	}
	tsz=n;
	find_rt(1,0);
	RT=rt;
	build(rt);
	while(l<=r-eps)
	{
		mid=(l+r)/2;
		if(check())
		l=mid,ans=mid;
		else r=mid;
	}
	printf("%.3lf\n",ans);
	return 0;
}

给题解代码写注释有助于深入理解

14.STA-Station @

换根DP

一眼切的那种裸题,刷个水奖励一下自己

sz没加还能过80分就很离谱

15.Freezing with Style ~

求边数在\([L,R]\)范围内,权值中位数最大的路径

看到中位数,首先肯定想到二分答案,根据题意可以把\(val\ge mid\)的边权设为1,剩下的设为-1,然后只需要验证是否存在一条长度在\([L,R]\)范围内,边权和为正的路径了。

显然要用到点分治,考虑比较暴力的做法,把做完的子树扔进一个线段树里面,线段树的区间表示边数,值表示范围内的最大值,那么最终时间复杂度则为\(O(nlog^3n)\)

看起来还需要优化掉一个\(log\)

我们发现,处理子树的时候,若是用BFS处理,其深度是单调递增的,也就是说所需要的范围一直在向一个方向平移,我们就可以考虑使用单调队列

16.Close Vertices

点分治的二维偏序问题

跟正常二维偏序一样,一维排序,一维树状数组

考虑到dep最大只有n,而且相比距离没有那么离散,所以dep那维树状数组,然后距离排序

感觉黑题高了,也就紫题吧

17.QTree4

动态点分治

考虑如果只有一个询问该怎么做

很显然,只需要分治的时候维护一下每个子树最远、次远的白色点的深度,相加更新答案即可

那么如果多组询问,并且修改,就需要动态点分治了。

对于建好的点分树,我们可以发现,对于点分树上一个节点x,其子树也一定在原树的子树里

对于一个节点x,我们维护其子树到其点分树父亲u的距离,用堆来存放,记为f[x],画图可知,在原图中,x的点分树子树就是u在原树中的某一个儿子的子树。那么考虑如何统计答案,对于一个节点x,答案就是其点分治子树中所有儿子f[x].top的最大值及次大值

posted @ 2021-03-13 16:21  岚默笙  阅读(88)  评论(0编辑  收藏  举报