wqs 二分学习笔记
wqs 二分好难懂啊,终于懂了的 Oken 决定写一篇尽量通俗易懂的学习笔记来帮助和我一样学不懂的人。
1
我们先看一道例题:Tree I,尝试由这道题推出 wqs 二分的性质。
给定 \(V\) 个点 \(E\) 条边的无向连通图 \(G\),第 \(i\) 条边的边权为 \(c_i\),颜色为 \(col_i\),其中 \(0\) 表示白色,\(1\) 表示黑色。求 \(G\) 的一棵生成树,恰好包含 \(need\) 条白边,最小化生成树中边权和。
\(V \leq 5 \times 10^4\),\(E \leq 10^5\),\(c_i \in [1,100]\),\(col_i \in \{0,1\}\)。
我们先抛开 \(need\) 的限制,显然地,我们可以使用 kruskal 求出最小生成树。
接下来考虑加上 \(need\) 的限制。我们定义函数 \(f(x)\) 表示恰好包含 \(x\) 条白边的生成树最小边权和,原问题即为求解 \(f(need)\)。
接下来是使用 wqs 二分需要满足的重要性质:\(f(x)\) 具有凸性。我们先假设 \(f(x)\) 具有凸性,接下来需要考虑在此前提下如何求解答案。
我们考虑在凸函数 \(f(x)\) 上进行二分,不过我们二分的是斜率。具体地,我们考虑用直线去切 \(f\)。容易发现,因为 \(f(x)\) 具有凸性,当切线的斜率单调变化时,切点的横坐标 \(x\) 也是单调变化的。我们需要求出 \(f(need)\),此时,我们考虑找到一条斜率为 \(k_0\) 的直线,使得其切 \(f\) 的切点为 \((need,f(need))\)。
考虑若这条直线的斜率为 \(k\),我们能求出哪些量。此时我们引出 wqs 二分的关键思想:带权二分。具体地,对于凸函数,其切线的截距一定取到极值,我们考虑截距对应的点,不妨令其为 \((0,g(k))\)。我们考虑如何计算出 \(g(k)\)。不妨令此时的切点为 \((x_0,f(x_0))\),我们可以得出 \(f(x_0)=g(k)+k \times x_0\)。接下来我们考虑将 \(x_0\) 相关的项移到一边,得到 \(g(k)=f(x_0)-k \times x_0\)。因为 \(g(k)\) 一定取到极值,所以 \(g(k)\) 为 \(f(x)-k \times x\) 的极值。在本题最小生成树的背景下,显然 \(f(x)\) 是下凸的,\(g(k)\) 应取到最小值,即 \(f(x)-k \times x\) 的最小值。
我们考虑 \(f(x)-k \times x\) 的实际意义。我们发现,若生成树中包含 \(x\) 条白边,则生成树的权值要减去 \(k \times x\)。自然地,我们考虑将 \(G\) 中所有白边边权减去 \(k\),容易发现这样求出的最小生成树一定为 \(f(x)-k \times x=g(k)\),同时我们也可以求出使用的白边条数,即 \(x_0\),此时我们通过 \(f(x_0)=g(k)+k \times x_0\) 可以求出 \(f(x_0)\),至此我们已经知道了切点为 \((x_0,f(x_0))\),就可以继续二分了。
等等,我们貌似还没证明 \(f\) 的凸性?不过在此题的背景下,显然白边取太少或太多答案显然不优,感性理解可知答案具有凸性。实际比赛时,作为 OIer,没有必要严格证明,如果你感性理解是对的,或者你打了表发现具有凸性,这就够了。
至此,wqs 二分的第一道例题已经结束,接下来是代码环节。
咦,不是还有不少细节吗,你怎么给漏了?
比如二分上下界。容易发现,我们最终关心的只有切点,即只关心选取白边的数量,考虑所有白边的边权都比黑边的边权大或小时,选取的方案是固定的,所以我们二分的上下界只要超过值域大小即可。
比如二分的细节处理。我们在二分时可能会出现几个点切点的斜率相同的情况,此时我们不妨钦定优先取白边,这样切点就仍是单调的,二分就不用处理过多的细节了。注意最后的切点可能不是 \(need\),需要找到直线后计算。
至此,wqs 二分的第一道例题已经结束,接下来是代码环节。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,need;
long long ans;
struct Edge{
int u,v,w,col;
}edge[100000];
bool operator <(const Edge &lhs,const Edge &rhs){
if(lhs.w!=rhs.w){
return lhs.w<rhs.w;
}
else{
return lhs.col<rhs.col;
}
}
int p[50010];
int Div_2(int num){
if(num%2==0 || num>=0) return num/2;
else return num/2-1;
}
int Find(int pos){
if(p[pos]!=pos) p[pos]=Find(p[pos]);
return p[pos];
}
int calc_id(int mid){
for(int i=0;i<n;i++){
p[i]=i;
}
for(int i=0;i<m;i++){
if(edge[i].col==0){
edge[i].w-=mid;
}
}
sort(edge,edge+m);
int tot_white=0,sum=0;
for(int i=0;i<m;i++){
int u=Find(edge[i].u),v=Find(edge[i].v),w=edge[i].w,col=edge[i].col;
if(u!=v){
sum+=w;
if(col==0) tot_white++;
p[u]=v;
}
}
ans=sum+mid*need;
for(int i=0;i<m;i++){
if(edge[i].col==0){
edge[i].w+=mid;
}
}
return tot_white;
}
int main(){
scanf("%d %d %d",&n,&m,&need);
for(int i=0;i<m;i++){
scanf("%d %d %d %d",&edge[i].u,&edge[i].v,&edge[i].w,&edge[i].col);
}
int L=-114,R=114;
while(L<R){
int mid=Div_2(L+R);
if(calc_id(mid)>=need){
R=mid;
}
else{
L=mid+1;
}
}
calc_id(L);
printf("%lld",ans);
return 0;
}
2
接下来再看一道简单题:种树。
给定长度为 \(n\) 的序列 \(a\),求在 \(a\) 中选出最多 \(k\) 个不相邻的数之和的最大值。
\(1 \leq n \leq 3 \times 10^5\),\(1 \leq k \leq \dfrac{n}{2}\),\(|a_i| \leq 10^6\)。
仿照上面的例子,我们先考虑去除 \(k\) 的限制,容易发现这是经典的 dp 问题。我们令 \(dp_{i,j}\) 表示考虑到了前 \(i\) 个数,第 \(i\) 个数选或不选的方案数,则此时有:
现在加上 \(k\) 的限制。我们当然可以在 dp 时再加一维,但这显然会超时。此时我们考虑 wqs 二分。令 \(f(x)\) 表示在 \(a\) 中选出恰好 \(x\) 个不相邻的数之和的最大值,容易发现 \(f\) 是上凸的。我们二分出切线的斜率,仍然考虑截距 \(g(k)=f(x)-k \times x\),发现其实际意义为将选出的每个数减去 \(k\)。我们当然可以先对 \(a\) 中的所有数减 \(k\),然后再做普通的 dp。于是这道题就结束了。
等等,好像还没有结束喵。我们考虑一件事,在 \(f(x)\) 的定义中,其对应的是恰好取 \(x\) 个数,而原问题中为最多取 \(x\) 个数。我们考虑这样会产生什么错误。不妨令 \(x=x_0\) 时 \(f(x)\) 取到最大值,则在 \(x>x_0\) 时,\(f(x) \leq f(x_0)\),此时我们可以恰好取 \(x_0\) 个数,这样答案一定不劣。对应到 wqs 二分中,我们只需要在二分时将 \(k\) 的斜率下界设为 \(0\) 即可。对应的,我们可以发现,在 wqs 二分时,将斜率设为 \(0\) 可以求出 \(f\) 的极值点。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const long long INF=1e9;
long long a[300010],dp[300010][2],ans;
int n,k,cnt[300010][2];
int calc_id(long long mid){
for(int i=1;i<=n;i++){
dp[i][0]=dp[i][1]=0;
cnt[i][0]=cnt[i][1]=1;
}
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]);
if(dp[i-1][0]<dp[i-1][1]){
cnt[i][0]=cnt[i-1][1];
}
else if(dp[i-1][0]>dp[i-1][1]){
cnt[i][0]=cnt[i-1][0];
}
else{
cnt[i][0]=min(cnt[i-1][0],cnt[i-1][1]);
}
dp[i][1]=dp[i-1][0]+a[i]-mid;
cnt[i][1]=cnt[i-1][0]+1;
}
long long sum=max(dp[n][0],dp[n][1]),tot;
if(dp[n][0]<dp[n][1]){
tot=cnt[n][1];
}
else if(dp[n][0]>dp[n][1]){
tot=cnt[n][0];
}
else{
tot=min(cnt[n][0],cnt[n][1]);
}
ans=sum+k*mid;
return tot;
}
int main(){
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
}
long long l=0,r=INF;
while(l<r){
long long mid=(l+r)>>1;
if(calc_id(mid)<=k){
r=mid;
}
else{
l=mid+1;
}
}
calc_id(l);
printf("%lld",ans);
return 0;
}
通过上面两个例子,我们发现,wqs 二分在使用时需要满足凸性。同时我们发现,wqs 二分可以解决恰好,最多,最少等问题。而且在上一道例题中,我们发现 wqs 二分可以优化 dp 的状态,减少维数。
3
接下来我们看一道经典的老题:[IOI 2000] 邮局,[IOI 2000] 邮局,[IOI 2000] 邮局。
数轴上有 \(n\) 个点,第 \(i\) 个点的坐标为 \(x_i\)。你需要选出 \(m\) 个点作为关键点,最小化每个点到最近的关键点距离之和。
\(1 \leq m \leq n \leq 300\),\(1 \leq m \leq 30\),\(1 \leq x_i \leq 10000\)。
\(1 \leq m \leq n \leq 3000\),\(1 \leq m \leq 300\),\(1 \leq x_i \leq 10000\)。
\(1 \leq m \leq n \leq 5 \times 10^5\),\(0 \leq x_i \leq 2 \times 10^6\)。
首先我们考虑原题,即第一个数据范围。容易发现,对于一个关键点,其产生贡献的点为一段连续的区间。我们考虑先将所有点从小到大排序。定义 \(dp_{i,j}\) 为在前 \(i\) 个点中选取 \(j\) 个关键点,最小的距离总和。可以发现转移式应为:
其中 \(w(l,r)\) 表示在编号属于 \([l,r]\) 的点中选出一个作为关键点,到 \([l,r]\) 中所有点的距离和最小值。根据初中数学,此时关键点一定是 \([l,r]\) 中所有点的中位数。通过 \(O(n)\) 的预处理,我们可以在 \(O(1)\) 的时间复杂度内算出 \(w(l,r)\)。此时 dp 的复杂度为 \(O(n^2m)\),可以通过原题。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=300,M=30;
long long num[N+10],sum[N+10],dp[N+10][M+10];
long long w(int l,int r){
int lmid=(l+r)/2,rmid=(l+r)/2+(l+r)%2;
return (sum[r]-sum[rmid-1])-(sum[lmid]-sum[l-1]);
}
int main(){
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&num[i]);
sum[i]=sum[i-1]+num[i];
}
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=0;k<i;k++){
dp[i][j]=min(dp[i][j],dp[k][j-1]+w(k+1,i));
}
}
}
printf("%lld",dp[n][m]);
return 0;
}
考虑常见的 dp 优化方法。容易发现在本题的背景下,dp 的决策点具有决策单调性。具体的,我们发现若 \(dp_{i,j}\) 的决策点为 \(pos_{i,j}\),则在 \(i\) 增加时,对于相同的 \(j\),为了均摊距离的贡献,其决策点也是不断向右移动的,采用决策单调性分治即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const long long INF=0x3f3f3f3f3f3f3f3f;
const int N=3000,M=300;
long long num[N+10],sum[N+10],lst[N+10],dp[N+10];
long long w(int l,int r){
int lmid=(l+r)/2,rmid=(l+r)/2+(l+r)%2;
return (sum[r]-sum[rmid-1])-(sum[lmid]-sum[l-1]);
}
void solve(int l,int r,int l_p,int r_p){
if(l>r){
return ;
}
int mid=(l+r)>>1;
int pos=-1;
for(int i=l_p;i<=r_p;i++){
if(i<mid){
long long prenum=lst[i]+w(i+1,mid);
if(pos==-1 || prenum<dp[mid]){
dp[mid]=prenum;
pos=i;
}
}
}
solve(l,mid-1,l_p,pos);
solve(mid+1,r,pos,r_p);
}
int main(){
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&num[i]);
sum[i]=sum[i-1]+num[i];
dp[i]=INF;
}
for(int i=1;i<=m;i++){
for(int j=0;j<=n;j++){
lst[j]=dp[j];
}
solve(1,n,0,n-1);
}
printf("%lld",dp[n]);
return 0;
}
欸你前面写了这么多,和 wqs 二分有关系吗?
我们考虑 \(1 \leq m \leq n \leq 10^5\) 的部分怎么做。我们发现,在 dp 的过程中有两个维度。为了优化,我们必须考虑减少一维。而 \(n\) 这一维由于涉及到关键点的贡献计算,不能删除。于是考虑删除 \(m\) 的这一维。发现 \(m\) 为关键点个数的限制,考虑使用 wqs 二分技巧。容易证明,令 \(f(x)\) 表示选择 \(x\) 个关键点的最小距离之和,则对于 \(x_1 \leq x_2\),有 \(f(x_1) \geq f(x_2)\)。但是 wqs 二分需要的是凸性,在此处,我们还可以说明 \(f\) 是下凸的。感性理解一下,对于 \(x\) 较小的情况,每增加一个关键点能够将一个较大的区间拆成两个区间,对答案的贡献也就较大。仍然考虑截距 \(g(k)=f(x)-k \times x\),发现其实际意义为对每个关键点的贡献减 \(k\)。考虑此时的 dp 状态转移方程为:
容易发现此时的转移仍然具有决策单调性,但因为这里 dp 的转移是依赖前面的值的,所以不能使用分治技巧。我们考虑使用二分队列的手法。具体地,考虑每一个点作为决策点时所能贡献的区间。我们采用队列维护贡献区间,可以发现每新加一个点会更新一段后缀的贡献点,实现的具体细节见代码。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<deque>
using namespace std;
const long long INF=0x3f3f3f3f3f3f3f3f;
const int N=500000,M=500000;
int n,m;
long long num[N+10],sum[N+10],dp[N+10],cnt[N+10],ans;
long long w(int l,int r){
int lmid=(l+r)/2,rmid=(l+r)/2+(l+r)%2;
return (sum[r]-sum[rmid-1])-(sum[lmid]-sum[l-1]);
}
long long Div_2(long long num){
if(num%2==0 || num>=0){
return num/2;
}
else{
return num/2-1;
}
}
struct Node{
int l,r,pos;
};
long long calc(int pos,int i,long long mid){
return dp[pos]+w(pos+1,i)-mid;
}
int calc_id(long long mid){
for(int i=1;i<=n;i++){
dp[i]=INF;
}
deque<Node> p_list;
p_list.push_back((Node){1,n,0});
for(int i=1;i<=n;i++){
Node pre=p_list.front();
p_list.pop_front();
dp[i]=calc(pre.pos,i,mid);
cnt[i]=cnt[pre.pos]+1;
if(pre.l<pre.r){
p_list.push_front((Node){pre.l+1,pre.r,pre.pos});
}
int lst=n+1;
while(!p_list.empty()){
pre=p_list.back();
p_list.pop_back();
if(calc(i,pre.r,mid)<=calc(pre.pos,pre.r,mid)){
if(calc(i,pre.l,mid)<=calc(pre.pos,pre.l,mid)){
lst=pre.l;
}
else{
int l=pre.l,r=pre.r+1;
while(l<r){
int id=(l+r)>>1;
if(calc(i,id,mid)<=calc(pre.pos,id,mid)){
r=id;
}
else{
l=id+1;
}
}
if(pre.l<l){
p_list.push_back((Node){pre.l,l-1,pre.pos});
}
lst=l;
break;
}
}
else{
p_list.push_back(pre);
break;
}
}
if(lst<=n) p_list.push_back((Node){lst,n,i});
}
ans=dp[n]+mid*m;
return cnt[n];
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&num[i]);
sum[i]=sum[i-1]+num[i];
}
long long l=-1e9,r=0;
while(l<r){
long long mid=Div_2(l+r);
if(calc_id(mid)>=m){
r=mid;
}
else{
l=mid+1;
}
}
calc_id(l);
printf("%lld",ans);
return 0;
}
4
接下来是一道省选题:林克卡特树。
有一棵 \(n\) 个节点的树,第 \(i\) 条边的边权为 \(v_i\)。你需要删除恰好 \(k\) 条边并连接恰好 \(k\) 条边权为 \(0\) 的边使其成为一棵新树,求新树的直径最大值。
\(0 \leq k < n \leq 3 \times 10^5\),\(|v_i| \leq 10^6\)。
首先我们考虑转化题意。选择恰好 \(k\) 条边删去,此时我们发现原来的树被分成了 \(k+1\) 个连通块。新树的直径,应为每个连通块的直径之和。所以我们可以得知,原问题等价于从树上选择 \(k+1\) 条不相交的链的边权和最大值。
这个时候显然可以 dp 了,但是 \(k\) 的限制仍然很讨厌。能不能消去 \(k\) 呢?等等,怎么又是这个问题。同样地,令 \(f(x)\) 表示选择 \(x\) 条不相交的链的边权和最大值。发现链太少会有边覆盖不到,链太多就会有很多退化成点的链,合理猜测 \(f\) 是上凸的。那就可以 wqs 二分了。注意为了避免因斜率相同而出错的情况,我们应当选取尽量少的链。
仍然考虑截距 \(g(k)=f(x)-k \times x\),发现其实际意义为对每条链的边权和减 \(k\)。接下来我们就可以进行 dp 了。定义 \(dp_{i,j}\) 表示在以 \(i\) 为根的子树中 \(i\) 的状态为 \(j\),选出若干条链的边权和的最大值。其中 \(dp_{i,0}\) 表示 \(i\) 不选,\(dp_{i,1}\) 表示 \(i\) 单独作为一条链,\(dp_{i,2}\) 表示 \(i\) 为一条链的一个端点,\(dp_{i,3}\) 表示 \(i\) 为子树内一条链的一个中间点。
对于 \(dp_{i,0}\),我们发现其为 \(i\) 的所有儿子任意取的情况。对于 \(dp_{i,1}\),贡献方式和 \(dp_{i,0}\) 差不多。对于 \(dp_{i,2}\),需要选取一个儿子贡献。对于 \(dp_{i,3}\),选择两个儿子进行贡献即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
int n,m;
const long long INF=-0x3f3f3f3f3f3f3f3f;
const int N=300000;
struct Node{
int v;
long long w;
};
vector<Node> G[N+10];
long long dp[N+10][4],ans;
int cnt[N+10][4];
long long Div_2(long long num){
if(num%2==0 || num>=0){
return num/2;
}
else{
return num/2-1;
}
}
struct Vex{
long long maxn;
int mint;
};
bool operator <(const Vex &lhs,const Vex &rhs){
if(lhs.maxn!=rhs.maxn) return lhs.maxn>rhs.maxn;
else return lhs.mint<rhs.mint;
}
void dfs(int u,int fa,long long mid){
int cnt_child=0;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;
long long w=G[u][i].w;
if(v!=fa){
cnt_child++;
dfs(v,u,mid);
}
}
if(cnt_child==0){
dp[u][0]=0;
cnt[u][0]=0;
dp[u][1]=-mid;
cnt[u][1]=1;
dp[u][2]=INF;
dp[u][3]=INF;
return ;
}
dp[u][0]=0;
cnt[u][0]=0;
dp[u][1]=-mid;
cnt[u][1]=1;
dp[u][2]=0;
cnt[u][2]=0;
dp[u][3]=mid;
cnt[u][3]=-1;
vector<Vex> son;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;
long long w=G[u][i].w;
if(v!=fa){
long long maxn=INF;
int mint;
long long tmp_maxn;
int tmp_mint;
if(dp[v][1]>maxn){
maxn=dp[v][1];
mint=cnt[v][1];
}
else if(dp[v][1]==maxn){
mint=min(mint,cnt[v][1]);
}
if(dp[v][2]>maxn){
maxn=dp[v][2];
mint=cnt[v][2];
}
else if(dp[v][2]==maxn){
mint=min(mint,cnt[v][2]);
}
tmp_maxn=maxn;
tmp_mint=mint;
if(dp[v][0]>maxn){
maxn=dp[v][0];
mint=cnt[v][0];
}
else if(dp[v][0]==maxn){
mint=min(mint,cnt[v][0]);
}
if(dp[v][3]>maxn){
maxn=dp[v][3];
mint=cnt[v][3];
}
else if(dp[v][3]==maxn){
mint=min(mint,cnt[v][3]);
}
dp[u][0]+=maxn;
cnt[u][0]+=mint;
dp[u][1]+=maxn;
cnt[u][1]+=mint;
dp[u][2]+=maxn;
cnt[u][2]+=mint;
dp[u][3]+=maxn;
cnt[u][3]+=mint;
son.push_back((Vex){tmp_maxn+w-maxn,tmp_mint-mint});
}
}
sort(son.begin(),son.end());
if(son.size()>=2){
dp[u][2]+=son[0].maxn;
cnt[u][2]+=son[0].mint;
dp[u][3]+=son[0].maxn;
cnt[u][3]+=son[0].mint;
dp[u][3]+=son[1].maxn;
cnt[u][3]+=son[1].mint;
}
else{
dp[u][2]+=son[0].maxn;
cnt[u][2]+=son[0].mint;
dp[u][3]=INF;
}
}
int calc_id(long long mid){
dfs(1,-1,mid);
long long maxn=INF;
int mint;
if(dp[1][1]>maxn){
maxn=dp[1][1];
mint=cnt[1][1];
}
else if(dp[1][1]==maxn){
mint=min(mint,cnt[1][1]);
}
if(dp[1][2]>maxn){
maxn=dp[1][2];
mint=cnt[1][2];
}
else if(dp[1][2]==maxn){
mint=min(mint,cnt[1][2]);
}
if(dp[1][0]>maxn){
maxn=dp[1][0];
mint=cnt[1][0];
}
else if(dp[1][0]==maxn){
mint=min(mint,cnt[1][0]);
}
if(dp[1][3]>maxn){
maxn=dp[1][3];
mint=cnt[1][3];
}
else if(dp[1][3]==maxn){
mint=min(mint,cnt[1][3]);
}
ans=maxn+mid*m;
return mint;
}
int main(){
scanf("%d %d",&n,&m);
m++;
for(int i=1;i<n;i++){
int u,v;
long long w;
scanf("%d %d %lld",&u,&v,&w);
G[u].push_back((Node){v,w});
G[v].push_back((Node){u,w});
}
long long l=-1e9,r=1e9;
while(l<r){
long long mid=Div_2(l+r);
if(calc_id(mid)<=m){
r=mid;
}
else{
l=mid+1;
}
}
calc_id(l);
printf("%lld",ans);
return 0;
}
5
最后来看一道 IOI 题:[IOI 2016] aliens,这也是 wqs 二分在国外也被称作 aliens trick 的来源。
有一个 \(m \times m\) 的网格。你需要在网格中选择最多 \(t\) 个字正方形,使得每个正方形的主对角线都在网格的主对角线上,且选出的正方形可以覆盖给定的 \(n\) 个关键点。求选出的正方形面积并的最小值。
\(1 \leq t \leq n \leq 10^5\),\(1 \leq m \leq 10^6\)。
首先我们有最基础的观察:网格主对角线两边用正方形覆盖的区域是对称的,此时我们不妨先将所有关键点对称到主对角线一边。
考虑关键点 \((x,y)\),下文中对于所有关键点,令 \(x \leq y\),设当前正方形的两个顶点为 \((a,a)\) 与 \((b,b)\),且 \(a \leq b\),则正方形能覆盖 \((x,y)\) 的充分要条件为 \(a \leq x \leq b\) 且 \(a \leq y \leq b\),整理得到 \(a \leq x\) 且 \(b \geq y\)。
这时我们能发现一个重要性质:若存在关键点 \((a,b)\) 和 \((c,d)\),且 \(a \leq c\),\(b \geq d\),则点 \((c,d)\) 是无用的。若存在正方形能覆盖 \((a,b)\),则其必然能覆盖 \((c,d)\)。去掉这些点后,我们按横坐标递增将所有点排序,此时所有点的纵坐标必然递增。我们考虑 dp。
记 \(dp_{i,j}\) 为使用 \(j\) 个正方形覆盖前 \(i\) 个点,形成的最小面积并。容易发现,对于一个正方形,其覆盖的必然是连续的一段点,此时每个正方形的覆盖都要做到局部的最优。
对于 \(w(l,r)\) 的计算,具体细节可以见代码。
按照惯例,现在我们需要找到凸性。我们令 \(f(x)\) 为使用恰好 \(x\) 个正方形覆盖所有关键点的最小面积并。容易发现 \(f\) 一定是递减的。同时我们发现,大正方形浪费的面积会更多,不妨猜测 \(f\) 是下凸的。那就可以 wqs 二分了。
由于我不想再复制了,总之你考虑 \(g(k)\) 为每个正方形的贡献减 \(k\),容易发现此时 dp 方程:
发现此时的东西很像有决策单调性的样子啊!写一发试试,结果过了喵!
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<deque>
using namespace std;
const long long INF=0x3f3f3f3f3f3f3f3f;
int n,m,k,x[100010],y[100010],cnt[100010];
vector<int> G[1000010];
long long dp[100010],ans;
long long Div_2(long long num){
if(num>=0 || num%2==0) return num/2;
else return num/2-1;
}
long long w(int l,int r){
long long len1=y[r]-x[l]+1;
long long len2=max(0,y[l-1]-x[l]+1);
return len1*len1-len2*len2;
}
long long calc(int pos,int i,long long mid){
return dp[pos]+w(pos+1,i)-mid;
}
struct Node{
int l,r,pos;
};
int calc_id(long long mid){
for(int i=1;i<=n;i++){
dp[i]=INF;
}
deque<Node> p_list;
p_list.push_back((Node){1,n,0});
for(int i=1;i<=n;i++){
Node pre=p_list.front();
p_list.pop_front();
dp[i]=calc(pre.pos,i,mid);
cnt[i]=cnt[pre.pos]+1;
if(pre.l<pre.r){
p_list.push_front((Node){pre.l+1,pre.r,pre.pos});
}
int lst=n+1;
while(!p_list.empty()){
pre=p_list.back();
p_list.pop_back();
if(calc(i,pre.r,mid)<=calc(pre.pos,pre.r,mid)){
if(calc(i,pre.l,mid)<=calc(pre.pos,pre.l,mid)){
lst=pre.l;
}
else{
int l=pre.l,r=pre.r+1;
while(l<r){
int id=(l+r)>>1;
if(calc(i,id,mid)<=calc(pre.pos,id,mid)){
r=id;
}
else{
l=id+1;
}
}
if(pre.l<l){
p_list.push_back((Node){pre.l,l-1,pre.pos});
}
lst=l;
break;
}
}
else{
p_list.push_back(pre);
break;
}
}
if(lst<=n) p_list.push_back((Node){lst,n,i});
}
ans=dp[n]+mid*k;
return cnt[n];
}
int main(){
scanf("%d %d %d",&n,&m,&k);
while(n--){
int pre_x,pre_y;
scanf("%d %d",&pre_x,&pre_y);
pre_x++,pre_y++;
G[min(pre_x,pre_y)].push_back(max(pre_x,pre_y));
}
n++;
for(int i=1;i<=m;i++){
if(G[i].size()){
sort(G[i].begin(),G[i].end());
int pre_x=i,pre_y=G[i][G[i].size()-1];
if(pre_y>y[n]){
n++;
x[n]=pre_x;
y[n]=pre_y;
}
}
}
long long l=-1e12,r=0;
while(l<r){
long long mid=Div_2(l+r);
if(calc_id(mid)>=k){
r=mid;
}
else{
l=mid+1;
}
}
calc_id(l);
printf("%lld",ans);
return 0;
}
总结一下,wqs 二分是一种处理具有凸性的信息的方法。我们通过二分斜率,给一些值加权的方法,去除某个维度的限制。
在 OI 中,你不必严格证明凸性,单调性等内容,如果能够感性理解,在考场上可以直接把结论当作对的。还可以通过打表等手段,观察数值的性质,进而解决问题。
›⩊‹ ੭
⪩. .⪨
ฅ•ω•ฅ

浙公网安备 33010602011771号