DP优化——wqs二分

news/2024/9/21 18:54:30/文章来源:https://www.cnblogs.com/FloatingLife/p/18399206

在看 wqs 二分前建议先去看另一篇博客——斜率优化,对凸包等知识点有所了解。

介绍

wqs 二分最初由王钦石在他的 2012 年国家集训队论文中提出,也叫"带权二分",或者"dp凸优化",而从 IOI 2016 的 Aliens 题目开始,这种方法开始逐步在竞赛圈中有了一定的地位。在国内我们一般称为「wqs 二分」,而在国外一般称为「Alien Trick」。

适用题型

wqs 二分的题目一般有以下特点:

  • 题目内容形式为:有 \(n\) 个物品,从中选出 \(m\) 个,每个要求最后的权值最小/最大。
  • 直接 dp 设 \(f[i][j]\) 表示前 \(i\) 个选出 \(j\) 个物品的话,转移是 \(f[i][j]=min_k(f[k][j-1] + Val(k,i))\),其中\(Val(k,i)\) 表示这次转移带来的权值。时间复杂度无论如何都是 \(O(n^2)\) 及以上的。
  • 如果没有选 \(m\) 这个限制,那可以优化到更低复杂度,并且可以算出此时最优方案选的数的个数。
  • 选的个数越多,最终权值越小/越大,即如果设 \(g(x)\),表示选 \(x\) 可以得到的最小/最大圈子,那么 \(g(x)\) 的图像构成一个凸包。

当然 wqs 二分不止应用于 DP 题,具体看例题。

解法

假设 \(g(x)\) 的图像为上凸包,要求的是最大值,不妨画一下 \(g(x)\) 的大致图像(当然其实我们是一个点都求不出来的):

假设我们现在用一条直线 \(y=Kx+b\) 去切一个点 \((x,g(x))\),那么可以得到 \(g(x)=Kx+b\),即这个点的坐标也可以表示成 \((x,Kx+b)\)
又因为上凸包有个性质,一条斜率为 \(K\) 的直线在他与这个凸包的切点处截距最大,也就是说如果我们能求出这个最大截距,并知道此时的横坐标,就能知道那个切点的具体坐标了。
因为凸包的斜率是单调的,所以随着 \(K\) 的减小,切到的 \(x\) 也越大,所以可以二分这个 \(K\),我们可以根据切点的坐标去调整 \(K\) 直到切到 \((m,g(m))\) 为止,。


现在的问题就是怎么求最大截距,因为我们压根不知道这个凸包长什么样子。
会发现 \(b = g(x)-Kx\),定义 \(h(x) = g(x)-Kx\),如果我们能以较低的复杂度求出最大的 \(h(x)\) 以及此时的 \(x\),也就求出了我们要的东西。
考虑给 \(h(x)\) 定义一个合理的意义,不难发现他其实就是给每个物品多加了一个 \(-K\) 的权值(所以叫代权二分),选了这个数就要 \(-K\)
而我们要求 \(h(x)\) 的最大值是没有限制要选多少个的,所以 dp 时直接 \(f_i = max_j(f_j + Val(j,i) - K)\) 即可,比一开始那个少了一维,会更好求,具体的优化方法/求法因题目而异,在例题中会讲。
注意最后的求 \(g(x)\) 时,要记得把 \(Kx\) 加上。

关于wqs二分的实现细节也在例题中。


例题

忘情

把式子变成 $((\sum_{i=1}^n x_i)+1)^2 $,设 \(S\) 为前缀和,那么朴素的 dp 是:
\(f[i][j]\) 表示前 \(i\) 个数划分成 \(j\) 段的最小值,转移为 \(f[i][j]=\min_{0<=k<i}(f[k][j-1] + (S_i - S_j + 1)^2)\)
容易证明 \((a+b+1)^2 > (a+1)^2 + (b+1)^2\),也就是说分的段数越多答案越小,即按照上面的定义 \(g(x)\) 表示分 \(x\) 段的最小值,那么 \(g(x)\) 的图像应该是一个下凸壳:

二分一个斜率 \(K\),用斜率 \(K\) 的直线去切这个凸包,那么截距 \(b=h(x)=g(x)-Kx\),因为是下凸包,所以我们要求最小截距,即把一段的权值定义成 \(((\sum xi)+1)^2 - K\),然后去掉段数限制,求最小答案。
考虑对这个新的问题 \(dp\),设 \(dp[i]\) 表示前 \(i\) 个数的最小值,\(dp[i]=\min_{0<=j<i}(dp[j] + (S[i]-S[j]+1)^2 - K)\),因为还要求此时的横坐标 \(x\),所以还要额外记一个dp数组,转移也是显然的。
这是经典的斜率优化形式,可以用单调队列优化到 \(O(n)\),不会斜率优化的戳这里。
总时间复杂度 \(O(n \log n)\)

wqs 二分一些实现的细节:

  1. 这里因为是下凸包,所以斜率 \(K\) 是负的,但是为了方便二分时我们把他变成正的,所以 check 里 dp 变成 \(dp[i]=\min_{0<=j<i}(dp[j] + (S[i]-S[j]+1)^2 + K)\) , 原来二分要把斜率调大的就调小。
  2. 注意最后凸包可能会有一段斜率为 \(0\) 的线段,即可能 \(g(m-1)=g(m)\),那如果我们在 \(check\) 里的斜率优化dp,在 \(g\) 值相同时取的是靠左的点,那么二分写的就是: 如果返回的那个 \(x\) <=m,那就更新答案并把斜率调大(这里还认为斜率是负的,不进行 1. 的转换) ; 相反,如果我们在 \(check\) 里的斜率优化dp,在 \(g\) 值相同时取的是靠右的点,那么二分写的就是: 如果返回的那个 \(x\) >=m,那就更新答案并把斜率调小。看取的是靠左还是靠右只要看斜率优化维护凸包时写的是 >= 还是 >,> 就是取靠左的,>=就是取靠右的。

code

变量名稍有不同。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5,inf=1e18;
inline int read(){int w = 1, s = 0;char c = getchar();for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());return s * w;
}int n,m,a[N]; 
int dp[N],g[N];  //dp[i]表示前 i 个数分成若干段的最小值,g[i] 表示取到最小值分的段树 
int dq[N],l,r;
int calc(int j){  //求纵坐标 return dp[j]+a[j]*a[j]-2ll*a[j];
} 
void check(int mid){l=1,r=0;dp[0]=0,g[0]=0;   dq[++r]=0;  //放 0 不是 1,因为可以自成 1 段。 for(int i=1;i<=n;i++){while(l<r && ( calc(dq[l+1]) - calc(dq[l]) ) < (2ll * a[i] * (a[dq[l+1]] - a[dq[l]]))) l++;  //把开头斜率小于当前斜率的线段 pop 掉int j=dq[l];dp[i]=dp[j]+(a[i]-a[j]+1ll)*(a[i]-a[j]+1ll)+mid;g[i]=g[j]+1ll;while(l<r && ( calc(i) - calc(dq[r]) ) * ( a[dq[r]] - a[dq[r-1]]) < ( calc(dq[r]) - calc(dq[r-1] ) ) * ( a[i] - a[dq[r]] )) r--;  //维护凸壳dq[++r]=i; }
}
signed main(){n=read(),m=read();for(int i=1;i<=n;i++) a[i]=read(),a[i]+=a[i-1];int l=0,r=inf,mid,ans=0;   //实际上斜率是负的,但是移项之后:b=f(x)-kx,所以就干脆把 k 取成正的,这样在check里是每一段+mid,而不是-mid while(l<=r){mid=(l+r)>>1;check(mid);if(g[n]<=m) r=mid-1,ans=mid;else l=mid+1; }check(ans);printf("%lld\n",dp[n]-ans*m);  //这里要减掉 mid(也就是最后的 ans) 带来的贡献 return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/792750.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一次Java性能调优实践【代码+JVM 性能提升70%】

这是我第一次对系统进行调优,涉及代码和JVM层面的调优。如果你能看到最后的话,或许会对你日常的开发有帮助,可以避免像我一样,犯一些低级别的错误。本次调优的代码是埋点系统中的报表分析功能,小公司,开发结束后,没有Code Review环节,所以下面某些问题,也许在Code Rev…

orangepi5plus上搭建ubuntu22.04环境

本文是参考orangepi5plus官方手册进行ubuntu22.04环境搭建:Orange Pi - Orangepi(官方手册下载官网)进去之后,选择ubuntu的官方镜像,点击下载出现百度网盘的下载内容,在众多版本里面我下载了下面的文件夹(ubuntu22.04) 该文件夹里面包含的后缀是.img就是ubuntu22.04的镜…

【Azure Policy】分享Policy实现对Azure Activity Log导出到Log A workspace中

问题描述 使用Azure Policy服务,对公司内部全部的订阅下的Activity Log,都需要配置导出到Log A Workspace中。以下Policy规则可以实现此目的。Policy内容说明 在Policy Rule部分中,选择资源的类型为 "Microsoft.Resources/subscriptions", 效果使用 DeployIfNotE…

C#/.NET/.NET Core优质学习资料,干货收藏!

前言 今天大姚给大家分享一些C#/.NET/.NET Core优质学习资料,希望可以帮助到有需要的小伙伴。 什么是 .NET? .NET 是一个免费的、跨平台的、开源开发人员平台,用于构建许多不同类型的应用程序。 使用 .NET,可以使用多种语言、编辑器和库来构建 Web、移动、桌面、游戏和 IoT…

自我评估

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/SE2024这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/SE2024/homework/13243这个作业的目标 熟悉Github和博客园,了解其基本操作,完善自我信息并学习Markdown的基本使用方法以及AIGC画图学号 102202132一…

2.常量 变量 类型转换 进制转换

常量 变量 类型转换 进制转换1.常量的使用2.变量的使用3.会强制类型转换一、常量(一)常量的分类1.概述:在代码的运行过程中,其值不会发生改变的数据2.分类: 整数常量:所有整数 小数常量:所有带小数点的 字符常量:带单引号,有且只能有一个内容 字符串常量:带双引号…

linux中tar命令的使用

tar简介 在linux中tar是一个常用的工具,用于打包和解压文件,全称是tape archive。 它能够将一组文件和目录打包成单个归档文件,也可以从归档文件中提取出文件和目录参数列表 使用实例介绍1.要创建一个归档文件,可以使用参数 -c 和 -f ,然后指定归档文件名例如要将/home/cy…

11.面向对象(3)

MODULE 11 面向对象会定义接口会在接口中定义抽象方法,默认方法,静态方法,成员变量会调用接口中的成员会利用多态的方式new对象知道多态的前提要知道使用多态的好处会在多态的前提下,向下转型会利用instanceof判断类型一.接口(一)接口的介绍1.接口:是一个引用数据类型,是一个…

8.封装

MODULE 8 封装1.要回使用private关键字修饰成员,并知道被private修饰之后的作用(访问特点)是什么2.会使用set方法为属性赋值,使用get方法获取属性值3.会利用this关键字区分重名的成员变量和局部变量4.会利用空参构造创建对象,并知道空参构造作用5.会使用有构造创建对象…

9.面向对象(1)

MODULE 9 面向对象1.会定义静态成员以及会调用静态成员2.会使用可变参数(会给可变参数传参)3.会二分查找(手撕)4.会冒泡排序(手撕)5.会debug的使用一、static关键字(一)static的介绍以及基本使用1.概述:static是一个静态关键字2.使用:(1)修饰一个成员变量:stat…

深入了解Vite:依赖预构建原理

前言 前面我们有提到Vite在开发阶段,提倡的是一个no-bundle的理念,不必与webpack那样需要先将整个项目进行打包构建。但是no-bundle的理念只适合源代码部分(我们自己写的代码),vite会将项目中的所有模块分为依赖与源码两部分。 依赖: 指的是一些不会变动的一些模块,如:…