dp凸优化/wqs二分学习笔记(洛谷4383 [八省联考2018]林克卡特树lct)

4 篇文章 0 订阅
1 篇文章 0 订阅

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

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

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

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

还是先从题目入手。

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

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

qwq
首先考虑应该怎么

暴力 d p dp dp

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

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

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

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

那么对于不同的 0 / 1 / 2 0/1/2 0/1/2我们需要分情况讨论。
对于 0 0 0来说,他可以从儿子的任意一个状态转移过来。
对于 1 1 1来说,首先他可以由当前状态的 1 1 1+儿子的 0 / 1 / 2 0/1/2 0/1/2中最大的那个转移,表示不与当前的儿子构成链。
也可以从当前状态的0+儿子的1+ v a l [ i ] val[i] 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 j j要倒着枚举,因为一个链只能被选一次(防止出现旧自己更新新自己的情况)
其次 z z z要循环到0(只是用来针对只算一条边的情况。)

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

凸优化

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

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

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

另外一种理解方式。

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

至于如何做不限制条数的收益,和 n k nk nk类似的 d p dp 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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值