• 心路历程


    心路历程

    写在前面

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

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

    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
    ",ans);
    	return 0;
    }
    

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

    14.STA-Station @

    换根DP

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

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

    15.Freezing with Style ~

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

    看到中位数,首先肯定想到二分答案,根据题意可以把(valge 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的最大值及次大值

  • 相关阅读:
    Chrome调试工具常用功能
    把读取sql的结果写入到excel文件
    Android逆向破解:Android Killer使用
    鸭子类型和猴子补丁
    Scrapy同时启动多个爬虫
    命令注入
    理解RESTful架构
    程序员需要谨记的九大安全编码规则
    10条建议分享:帮助你成为与硅谷工程师一样优秀的程序员
    代码审计:是安全专家都应该掌握的技能
  • 原文地址:https://www.cnblogs.com/Marcelo/p/14529303.html
Copyright © 2020-2023  润新知