心路历程
心路历程
写在前面
文字是感受的理性化,把自己做题时候的想法写出来,既可以理清思路,又能记录当时的想法,方便以后回忆
标识:@水题 ~未做完 *好题
1.贪吃的九头龙
明显的树形DP
m=2的时候,设置状态dp[x][t][1/0]表示x子树中已有t个被吃,x是否被大头吃
则有
最后取dp[rt][m][0/1]作为答案
当m>2时,则每条非大头吃的树枝都可以被避免,则有
初始值,都为0
很显然,不合理
显然还需要枚举子树已吃了多少个
2.巡逻
k=1时候很明显是求树的直径
k=2的时候只需要把树的直径求出来之后,把直径标成-1,再求一边树的直径
如果在考场上k=1还是能想出来的,k=2的时候,就要充分发挥算法的简便,即不要什么都想自己算出来
3.通往自由的钥匙 @
很显然的树上背包
一开始看错数据范围,以为不可做
注意的点就是每扇门去了就需要消耗能量
则可以设置状态为
F[x][k]表示以x为根的子树中已经消耗了k能量能获得的最大钥匙数量
则有
注意都要反向枚举
4.选课 @
树形背包模板,觉得不太熟悉这个模型就来练一练
很明显需要抽象建图,若选课b需要先学a,那么a就是b的父亲
设置状态F[x][k]表示到以x为根的子树已选择了k个能得到的最大学分
初始状态则有
对于每个节点,F[x][0]=0,F[x][1]=s[x];
之后,转移方程为
然后考虑枚举顺序
很显然,如果正向枚举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方,显然过不了
考虑优化,
我们考虑到转移方程为
则可以推断出,当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的最大值及次大值