zoukankan      html  css  js  c++  java
  • dp凸优化/wqs二分学习笔记(洛谷4383 [八省联考2018]林克卡特树lct)

    qwq
    安利一个凸优化讲的比较好的博客

    https://www.cnblogs.com/Gloid/p/9433783.html

    但是他的暴力部分略微有点问题
    qwq

    我还是详细的讲一下这个题+这个知识点吧。

    还是先从题目入手。

    首先我们分析题目。
    因为题目要删除(k)条边,然后再新建(k)条边,求两点的路径和。
    那我们不妨这么考虑,对于新连接一条边,相当于链接了原树上的两条链,且链不存在交点。
    那我们新建(k)条边,就相当于把原树上没有交的(k+1)条链连接起来。
    既然要求权值最大。

    那我们就可以直接把题目转换成求树上选出点不相交的(k+1)条链的最大收益。(每个点都是一条链)。

    qwq
    首先考虑应该怎么

    暴力(dp)

    由于对于(x)这颗子树,要分情况讨论他属于一条链的端点,中间点,还是不属于链。

    所以我们定义状态(dp[i][j][0/1/2])表示(i)的子树里,已经选了(j)条链,其中(i)这个点不属于链,属于一个链的端点,属于一个链的中心点的最大收益。

    首先,因为负权,所以要把所有的(dp)弄成初始值是(-inf)的,其中(dp[i][0][0])为0。

    然后我们考虑应该怎么转移。
    对于一个点来说,我们先枚举他的儿子,然后枚举他子树内的链数(j),枚举分配给他的儿子的链数(z)

    那么对于不同的(0/1/2)我们需要分情况讨论。
    对于(0)来说,他可以从儿子的任意一个状态转移过来。
    对于(1)来说,首先他可以由当前状态的(1)+儿子的(0/1/2)中最大的那个转移,表示不与当前的儿子构成链。
    也可以从当前状态的0+儿子的1+(val[i]),表示从当前儿子上来一条链。
    另外的一种情况就是只选与当前儿子相连的边。

    对于2来说,其实也是和1同理。

    void solve(int x,int fa)
    {	
      for (int i=point[x];i;i=nxt[i])
      {
        int p = to[i];
        if(p==fa) continue;
        solve(p,x);
        for (int j=min(k,size[x]);j>=1;j--)
        {
    	  for (int z=0;z<=min(j,size[p]);z++)
    	  {
    	     dp[x][j][2]=max(dp[x][j][2],dp[x][j-z][2]+max(dp[p][z][0],max(dp[p][z][1],dp[p][z][2])));
    		 dp[x][j][2]=max(dp[x][j][2],dp[x][j-z][1]+dp[p][z+1][1]+val[i]);
    		 if (j>z) dp[x][j][2]=max(dp[x][j][2],dp[x][j-z][1]+val[i]+dp[p][z][0]);
    	  	 //本身就有1一条链 
    		 dp[x][j][1]=max(dp[x][j][1],dp[x][j-z][1]+max(dp[p][z][0],max(dp[p][z][1],dp[p][z][2])));
    	  	 //和当前构成一条链
    		 dp[x][j][1]=max(dp[x][j][1],dp[x][j-z][0]+dp[p][z][1]+val[i]);
    		 if (j>z) dp[x][j][1]=max(dp[x][j][1],dp[x][j-z-1][0]+dp[p][z][0]+val[i]); 
    		 dp[x][j][0]=max(dp[x][j][0],dp[x][j-z][0]+max(dp[p][z][0],max(dp[p][z][1],dp[p][z][2]))); 
          }
    	}
      }
    }
    

    这里有两个需要注意的问题,首先是(j)要倒着枚举,因为一个链只能被选一次(防止出现旧自己更新新自己的情况)
    其次(z)要循环到0(只是用来针对只算一条边的情况。)

    然后通过以上的过程,我们就能轻松愉悦的通过(O(nk))的60分了。
    那么应该怎么优化这个过程呢。

    凸优化

    首先,我们通过做差分,即(ans[i][j]-ans[i][j-1])(ans[i][j])表示(n=i,k=j)时候的答案,发现斜率是逐渐降低的,那我们不难发现在(i)一定的情况下,其实(ans)是关于(j)的上凸函数。(并且一定要满足斜率单调!!!!!!)、

    那我们实际上就是要求出来在(j=k)的时候的那个对应的凸包的点是多少。
    根据斜率单调
    我们可以直接二分一个(mid),然后让所有的边权都加上(mid),然后不限制选的链的条数,求最大收益和链数,这个复杂度是(O(n))的。

    因为我们发现,当(mid= inf)的时候,一定是选(n)条,当(inf=-mid)的时候,是选0条,那么因为斜率单调(所以选择的条数一定是随着mid单调变大的),所以我们一定能通过改变这个(mid),存在某一个(mid)满足恰好选择(k)条,那么正好符合题目要求。

    另外一种理解方式。

    qwq这个理解方式,每次要减去(mid),其实本质是一样的。
    我们相当于对于每个点求出他的正上方所对应的凸包的点是啥,那么我们通过(erf)这个东西,相当于把原图的上的每个点(x)向下移动(mid*x),那么我们可以发现,随着(mid)的变大,每次随便选的取到的收益的最高的地方,是单调右移的。我们只需要记录一下选的最大收益和链数,然后找到链数等于(k)所对应的(mid),计算一遍贡献就行。

    至于如何做不限制条数的收益,和(nk)类似的(dp)

    感觉这个东西要感性+理性啊

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<cmath>
    #include<queue>
    #include<map>
    #include<set>
    #define mk make_pair
    #define ll long long
    #define int long long
    using namespace std;
    inline int read()
    {
      int x=0,f=1;char ch=getchar();
      while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
      while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
      return x*f;
    }
    const int maxn = 3e5+1e2;
    const int maxm = 2*maxn;
    const int inf = 1e12;
    int point[maxn],nxt[maxm],to[maxm],val[maxm];
    int n,m,cnt;
    int l,r,ans,k;
    struct ymh{
    	int val,num;
    	ymh operator + (ymh b)
    	{
    		return (ymh){val+b.val,num+b.num};
    	}
    }; 
    ymh max(ymh a,ymh b)
    {
    	if(a.val==b.val) 
    	{
    		if(a.num<b.num) return a;
    		else return b;
    	}
    	else
    	{
    		if(a.val<b.val)  return b;
    		else return a;
    	}
    }
    void addedge(int x,int y,int w)
    {
    	nxt[++cnt]=point[x];
    	to[cnt]=y;
    	val[cnt]=w;
    	point[x]=cnt;
    }
    ymh dp[maxn][3];
    void dfs(int x,int fa,int lim)
    {
    	dp[x][0]=(ymh){0,0};
    	dp[x][1]=(ymh){-inf,-inf};//不存在这种情况 
    	dp[x][2]=(ymh){lim,1};//可以将一个点看成一条链 
    	for (int i=point[x];i;i=nxt[i])
    	{
    		int p = to[i];
    		if (p==fa) continue;
    		dfs(p,x,lim); 
    		ymh now = max(dp[p][0],max(dp[p][1],dp[p][2]));
    		dp[x][2]=max(dp[x][2],dp[x][2]+now); //本身就有链 
    		dp[x][2]=max(dp[x][2],dp[x][1]+dp[p][0]+(ymh){val[i],0}); //新加一条边 
    		dp[x][2]=max(dp[x][2],dp[x][1]+dp[p][1]+(ymh){val[i]-lim,-1});//用一条边合并两个链(链数会减一)
    		dp[x][1]=max(dp[x][1],dp[x][1]+now);
    		dp[x][1]=max(dp[x][1],dp[x][0]+dp[p][1]+(ymh){val[i],0});//沿着之前的链 
    		dp[x][1]=max(dp[x][1],dp[x][0]+dp[p][0]+(ymh){val[i]+lim,1});//新增一个链
    		dp[x][0]=max(dp[x][0],dp[x][0]+now); 
    	}
    }
    signed main()
    {
      n=read(),k=read();
      for (int i=1;i<n;i++)
      {
      	 int x=read(),y=read(),w=read();
      	 addedge(x,y,w);
      	 addedge(y,x,w);
      	 r+=w;
      }
      k++;
      r=inf,l=-inf;
      while(l<=r)
      {
      	 int mid = l+r >> 1;
      	 memset(dp,0,sizeof(dp));
      	 dfs(1,0,mid);
      	 ymh now = max(dp[1][0],max(dp[1][1],dp[1][2]));
      	 if (now.num<=k) ans=mid,l=mid+1;
      	 else r=mid-1;
      }
      memset(dp,0,sizeof(dp));
      dfs(1,0,ans);
      ymh now = max(dp[1][0],max(dp[1][1],dp[1][2]));
      cout<<now.val-k*ans;
      return 0;
    }
    
    
  • 相关阅读:
    TIF转JPG
    跨线程取出控件的值的写法(不是跨线程赋予控件值)
    oracle根据正则表达式查找对应的字段
    oracle数据库连接字符串
    access检测表没有的字段,添加之
    解决 Unable to load DLL 'OraOps9.dll': 找不到指定的模块。 (Exception from HRESULT: 0x8007007E)
    oracle关键字使用
    to_number,Extract oracle的关键字
    OracleCommand.CommandText 无效
    调用带参数的线程两种方法
  • 原文地址:https://www.cnblogs.com/yimmortal/p/10197670.html
Copyright © 2011-2022 走看看