WQS 二分学习笔记
目录
一、算法介绍
二、相关例题
例1、
例2、
例3、
例4、
例5、
例6、
例7、
一、算法介绍
二分又称带权二分、凸优化,因王钦石神仙最先提出而得名。
二分能够解决的问题类型:
有若干个物品可供选择,某些特殊物品限制恰好选 个,求最优方案。
前提条件:
- 记 为限制恰好选 个的答案,那么 是凸函数。
- 去掉限制后可以快速计算答案。
凸性是根据题目性质分析出来的,我们事先并不知道凸包的形状,同时目标变为计算 。
二分干的事情是二分斜率,快速计算斜率为 的直线会切在凸包上的哪个点。
注意到 对应的截距为 ,可以看成**将每个特殊物品的代价都减去 **。
以上凸壳为例,求切点等价于求 的最大值。
而这等价于将每个特殊物品的代价减去 后,计算没有特殊限制的答案。
对于大多数题目, 为整数,那么斜率 也为整数,使用整数二分就足够了。
只有当 为实数时,才需要使用实数二分。
二分边界应为斜率的最值,即 。
二分思路还算清晰,然而三点共线的细节非常搞心态。
换一种方式理解:目标找到使得切点横坐标 的最大(上凸)或最小(下凸)斜率 。
正确操作:记二分到的切点为 ,在闭区间的一侧用 (而不是 )更新答案。
如果直线切在凸包上的多个位置(凸包上某一段斜率与二分斜率相同),根据目标,我们需要返回横坐标最大的切点。
///上凸函数,切点在右边时应增大斜率
while(r-l>1)///左闭右开
{
int mid=(l+r)/2;
if(/**切点横坐标**/>=m) l=mid,res=/**切点纵坐标**/+mid*m;
else r=mid;
}
///下凸函数,切点在右边时应减小斜率
while(r-l>1)///左开右闭
{
int mid=(l+r)/2;
if(/**切点横坐标**/>=m) r=mid,res=/**切点纵坐标**/+mid*m;
else l=mid;
}
二、相关例题
例1、
题目描述
给定一张 个点, 条边的带权无向图,每条边为黑色或白色。
求一棵权值和最小且恰有 条白色边的最小生成树。
数据范围
- 。
时间限制 ,空间限制 。
分析
记 为恰有条白色边的最小生成树权值之和,不存在为 。
关于 为下凸函数的证明:
可以看成仅用黑边跑 算法(不存在的边权值视为 ),那么 就是在 的基础上,加入一条白边,再从环上删掉一条权值最大的黑边。
如果第 次替换的增量 比第 次的 小,那么对于 ,我们放弃第 次替换,执行第 次替换,容易发现这也是一棵合法的生成树。
但 ,这与 的最优性矛盾。
因此 ,即 为下凸函数。
二分斜率 ,将所有白边的权值减去 后跑 ,切点横坐标为白边个数。
注意为了保证最优位置横坐标最大,我们要尽可能多的使用白边,在排序时边权相同的白边在前。
时间复杂度 。
可以将黑边和白边分别排序后做归并,时间复杂度降至 ,但是我懒。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5;
int k,m,n,res;
int f[maxn];
struct edge
{
int u,v,w,c;
}e[maxn];
bool cmp(edge a,edge b)
{
if(a.w!=b.w) return a.w<b.w;
return a.c<b.c;
}
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
pii calc(int x)
{
for(int i=0;i<n;i++) f[i]=i;
for(int i=1;i<=m;i++) if(!e[i].c) e[i].w-=x;
sort(e+1,e+m+1,cmp);
int cnt=0,val=0;
for(int i=1;i<=m;i++)
{
int u=find(e[i].u),v=find(e[i].v);
if(u==v) continue;
f[u]=v,cnt+=!e[i].c,val+=e[i].w;
}
for(int i=1;i<=m;i++) if(!e[i].c) e[i].w+=x;
return mp(cnt,val);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++) scanf("%d%d%d%d",&e[i].u,&e[i].v,&e[i].w,&e[i].c);
int l=-101,r=100;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
printf("%d\n",res);
return 0;
}
例2、
题目描述
给定一张 个点, 条边的带权无向图,要求 号点恰好连了 条边,求权值和最小的生成树,无解输出 Impossible
。
数据范围
- 。
时间限制 ,空间限制 。
分析
先判掉无解的情况:
- 原图不连通。
- 给边去重后, 的出边数量 。
- 加入所有与 无关的边后,连通块(不包含 )数量 。
记 为限制 恰好连接 条边的最小权值,不存在为 。
关于 为下凸函数的证明:
可以看成在 的基础上,加入一条从 出发的边,再从环上删掉一条与 无关的边。
记 ,如果 ,我们放弃第 次替换,转而执行第 次替换,容易发现这仍然是一棵合法的 的生成树,但与 的最小性矛盾。
因此 ,即 为下凸函数。
接下来套一个 二分板子就可以了。
本题数据范围较大,用归并排序实现,时间复杂度 。
#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,ll>
using namespace std;
const int maxn=5e4+5;
int k,m,n,s;
ll res;
int f[maxn];
bitset<maxn> vis;
struct edge
{
int u,v,w;
};
vector<edge> v1,v2;
inline int read()
{
int q=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) q=10*q+ch-'0',ch=getchar();
return q;
}
bool cmp(edge a,edge b)
{
return a.w<b.w;
}
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
bool merge(int u,int v)
{
u=find(u),v=find(v);
if(u==v) return false;
return f[u]=v,true;
}
pii calc(int x)
{
static int a=v1.size(),b=v2.size();
for(int i=1;i<=n;i++) f[i]=i;
ll cnt=0,val=0;
for(int i=0,j=0;i<a||j<b;)
{
if(i<a&&(j>=b||v1[i].w-x<=v2[j].w))
{
if(merge(v1[i].u,v1[i].v)) cnt++,val+=v1[i].w-x;
i++;
}
else
{
if(merge(v2[j].u,v2[j].v)) val+=v2[j].w;
j++;
}
}
return mp(cnt,val);
}
int main()
{
n=read(),m=read(),s=read(),k=read();
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),w=read();
if(u==v) continue;
if(v==s) swap(u,v);
if(u==s) vis[v]=true,v1.push_back({u,v,w});
else merge(u,v),v2.push_back({u,v,w});
}
if(vis.count()<k) printf("Impossible\n"),exit(0);
vis.reset();
for(int i=1;i<=n;i++) if(i!=s) vis[find(i)]=true;
if(vis.count()>k) printf("Impossible\n"),exit(0);
for(auto p:v1) merge(p.u,p.v);
for(int i=1;i<=n;i++) if(find(i)!=find(1)) printf("Impossible\n"),exit(0);
sort(v1.begin(),v1.end(),cmp);
sort(v2.begin(),v2.end(),cmp);
int l=-3e4-1,r=3e4;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
printf("%lld\n",res);
return 0;
}
例3、
参考题解。
题目描述
给定一张 个点, 条边的带权无向图,保证图中没有重边、自环。
求一棵满足 的边权和最小的生成树,输出方案,无解输出 -1
。
数据范围
- 。
时间限制 ,空间限制 。
分析
容易发现本题其实比上一题就多了一个输出方案。
记从 出发的边为白边,其余为黑边。
网上常见的错解:将白边边权减去 后跑 ,跑到 条白边后忽略后面的所有黑边。
原因在于 二分仅能保证存在一种恰有 条白边的方案,但上述构造却指定了这 条白边,因此很容易让答案偏大或返回无解。
然而原题数据太水全放过了,下面是正确的构造方法:
先用最少白边的策略跑 ,固定所有白边,考虑在它的基础上构造方案。
再用最多白边的策略跑 ,记 为尚未固定的边权大于 的白边数量最小值。
保留所有白边,对于第 层(边权为 的所有边),优先使用白边,直到满足 ,层内其他边再使用黑边。
这个做法的正确性在于,根据 算法的性质,每一层连完以后连通块形状固定,而我们已经考虑了后面所有层的限制。
时间复杂度 。
拍了几万组数据,应该没假。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5;
int a,b,k,m,n,u,v,w,res;
int f[maxn];
bitset<maxn> vis;
map<int,int> lim;
struct edge
{
int u,v,w,id;
};
vector<edge> v1,v2;
bool cmp(edge a,edge b)
{
return a.w<b.w;
}
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
bool merge(int u,int v)
{
u=find(u),v=find(v);
if(u==v) return false;
return f[u]=v,true;
}
pii calc(int x)
{
for(int i=1;i<=n;i++) f[i]=i;
int cnt=0,val=0;
for(int i=0,j=0;i<a||j<b;)
{
if(i<a&&(j>=b||v1[i].w-x<=v2[j].w))
{
if(merge(v1[i].u,v1[i].v)) cnt++,val+=v1[i].w-x;
i++;
}
else
{
if(merge(v2[j].u,v2[j].v)) val+=v2[j].w;
j++;
}
}
return mp(cnt,val);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
if(u==1||v==1) v1.push_back({u,v,w,i});
else merge(u,v),v2.push_back({u,v,w,i});
}
if(v1.size()<k) printf("-1\n"),exit(0);
for(int i=2;i<=n;i++) vis[find(i)]=1;
if(vis.count()>k) printf("-1\n"),exit(0);
for(auto p:v1) merge(p.u,p.v);
for(int i=2;i<=n;i++) if(find(i)!=find(1)) printf("-1\n"),exit(0);
sort(v1.begin(),v1.end(),cmp);
sort(v2.begin(),v2.end(),cmp);
a=v1.size(),b=v2.size(),vis.reset();
int l=-1e5-1,r=1e5;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
for(auto &p:v1) p.w-=r;
for(int i=1;i<=n;i++) f[i]=i;
int cur=0;///第一遍以最少白边跑kruskal,强制这些白边在最终方案出现,cur为最小白边数
for(int i=0,j=0;i<a||j<b;)
{
if(i<a&&(j>=b||v1[i].w<v2[j].w))
{
if(merge(v1[i].u,v1[i].v)) cur++,vis[v1[i].id]=1;
i++;
}
else merge(v2[j].u,v2[j].v),j++;
}
for(int i=1;i<=n;i++) f[i]=i;
for(auto p:v1) if(vis[p.id]) merge(p.u,p.v);
int now=0;///第二遍以最多白边跑kruskal,lim[w]为边权大于w的自由白边数量最小值
for(int i=0,j=0;i<a||j<b;)
{
static int val=0;
if(i<a&&(j>=b||v1[i].w<=v2[j].w)) now+=merge(v1[i].u,v1[i].v),val=v1[i++].w;
else merge(v2[j].u,v2[j].v),val=v2[j++].w;
lim[val]=now;
}
for(auto &p:lim) p.se=max(k-cur-p.se,0);
for(int i=1;i<=n;i++) f[i]=i;
for(auto p:v1) if(vis[p.id]) merge(p.u,p.v);
for(int i=0,j=0,now=cur;i<a||j<b;)
{///第三遍kruskal,在now+lim[w]<=k的限制下尽可能多选白边
if(i<a&&(j>=b||v1[i].w<=v2[j].w))
{
if(now+lim[v1[i].w]<k&&merge(v1[i].u,v1[i].v)) now++,vis[v1[i].id]=1;
i++;
}
else
{
if(merge(v2[j].u,v2[j].v)) vis[v2[j].id]=1;
j++;
}
}
printf("%d\n",n-1);
for(int i=1;i<=m;i++) if(vis[i]) printf("%d ",i);
putchar('\n');
return 0;
}
例4、
题目描述
有 个物品, 个 类球和 个 类球, 类球和 类球抓到第 个物品的概率分别为 。
每类球最多在同一个物品上使用一次,但一个物品可以同时使用两类球。求期望抓到物品个数的最大值,要求绝对或相对误差 。
数据范围
- 。
时间限制 ,空间限制 。
分析
容易想到一个 的 , 表示考虑前 个物品,使用 个 类球和 个 类球的最大收益。
转移方程如下:
大胆猜测 关于 是上凸函数。
用 二分优化,相当于 类球数量不限,但每使用一次会产生 的代价。
于是 降维后复杂度变为 。
注意需要实数二分,时间复杂度 。
本题凸性可以用费用流证明:
从源点向 类球分别连容量为 的边,从 类球向每个物品连容量为 ,费用为 的边,再从每个物品向汇点连边 ,跑最大费用最大流即可。
由于增广路长度非严格递减,因此代价关于流量是凸函数。
Warning:
- 二分套二分是假做法,因为 二分需要记录使用的 球数量最大值,而二分套二分需要记录在 球数量恰好为 的前提下,使用 球数量的最大值,但这个问题没有办法解决, 数据可以看这里。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const double eps=1e-9,inf=1e9;
int a,b,n;
double res,p[maxn],q[maxn];
struct node
{
double val;
int cnt;
};
node operator+(node a,node b)
{
return {a.val+b.val,a.cnt+b.cnt};
}
bool operator<(node a,node b)
{
if(fabs(a.val-b.val)>eps) return a.val<b.val;
return a.cnt<b.cnt;
}
node f[maxn][maxn];
node calc(double x)
{
for(int i=0;i<=n;i++) for(int j=0;j<=b;j++) f[i][j]={-inf,0};
f[0][0]={0,0};
for(int i=1;i<=n;i++)
for(int j=0;j<=b;j++)
{
f[i][j]=max(f[i-1][j],f[i-1][j]+(node){p[i]-x,1});
if(j) f[i][j]=max({f[i][j],f[i-1][j-1]+(node){q[i],0},f[i-1][j-1]+(node){p[i]+q[i]-p[i]*q[i]-x,1}});
}
return f[n][b];
}
int main()
{
scanf("%d%d%d",&n,&a,&b);
for(int i=1;i<=n;i++) scanf("%lf",&p[i]);
for(int i=1;i<=n;i++) scanf("%lf",&q[i]);
double l=0,r=1;
while(r-l>eps)
{
double mid=(l+r)/2;
node cur=calc(mid);
if(cur.cnt>=a) l=mid,res=cur.val+a*mid;
else r=mid;
}
printf("%.10lf\n",res);
return 0;
}
例5、
题目描述
有 天时间,第 天你可以花费 准备一道题、花费 打印一道题(一天内可以同时做这两件事),准备好的题可以留到以后打印。
你需要准备并打印 道题,求最小代价。
数据范围
- 。
时间限制 ,空间限制 。
分析
有一个显然的费用流建图,可以通过 medium 版本:
记源点为 ,汇点为 ,连边 。
由于费用关于流量为凸函数,考虑 二分。
准备一道题的代价为 ,用小根堆维护已经插入的 的值。
有一个很 的贪心想法,如果 加上堆顶小于零,那么我们选择这道题可以让总代价变小。
但是后面有可能出现更小的 ,考虑增加一个反悔操作,将所有已经选择的 也加入堆中。
如果仅需要记录最小代价那么已经做完了,但还需要求最大题数。
对于每种决策,同时维护选择它能带来的题数增量。具体的,给 赋权值 , 赋权值 。
根据 pair
双关键字比较的特性,小根堆中应该存储权值的相反数。
时间复杂度 。
#include<bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=5e5+5;
int k,n,res;
int a[maxn],b[maxn];
pii calc(int x)
{
int cnt=0,val=0;
priority_queue<pii,vector<pii>,greater<pii>> q;
for(int i=1;i<=n;i++)
{
q.push(mp(a[i]-x,-1));
if(b[i]+q.top().fi<=0)
{
val+=b[i]+q.top().fi,cnt-=q.top().se,q.pop();
q.push(mp(-b[i],0));
}
}
return mp(cnt,val);
}
signed main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<=n;i++) scanf("%lld",&b[i]);
int l=0,r=2e9;
while(r-l>1)
{
int mid=(l+r)/2;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
printf("%lld\n",res);
return 0;
}
例6、
题目描述
定义一个序列的权值为 。
给定长为 的序列 ,要求将其划分为 段,求每段权值之和的最小值。
数据范围
- 。
时间限制 ,空间限制 。
分析
记 , 表示将 划分为 段的最小代价,转移方程为:
可以感性理解一下为什么 是关于 的下凸函数:
用 代指上面的 ,记 。
可以看成在 的基础上切了一刀,假设切开的两段长度分别为 ,则 。
显然我们选择的 只会越来越小(否则与 的最优性矛盾),因此 为下凸函数。
注意这个证明并不严谨,因为 的方案并不一定能通过 切一刀得到。
考虑 二分,假设二分到斜率为,转移方程为:
对上述方程恒等变形:
可以看成用斜率为 的直线切平面上的点 能得到的最小截距,斜率优化可以做到线性。
时间复杂度 。
注意二分范围要覆盖所有可能的斜率,本题下界为 ,上界为 。
按照 二分的套路,转移代价相同时,我们要取段数最多的一个决策点,这意味着斜率相等时我们需要根据段数决策是否弹出队列。然而,本题不特判这个细节(判断是否弹队列时带等号)也能通过,而且笔者确实没能拍出 数据。
#include<bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
#define x(k) (2*(s[k]-1))
#define y(k) (f[k]+(s[k]-1)*(s[k]-1))
using namespace std;
const int maxn=1e5+5;
int h,m,n,t,res;
int f[maxn],g[maxn],q[maxn],s[maxn];
long double slope(int i,int j)
{
return 1.0L*(y(j)-y(i))/(x(j)-x(i));
}
pii calc(int X)
{
q[h=t=1]=0;
for(int i=1;i<=n;i++)
{
while(h<t&&mp(slope(q[h],q[h+1]),g[q[h]])<mp(1.0L*s[i],g[q[h+1]])) h++;
f[i]=f[q[h]]+(s[i]-s[q[h]]+1)*(s[i]-s[q[h]]+1)-X,g[i]=g[q[h]]+1;
while(h<t&&mp(slope(q[t-1],q[t]),g[i])>mp(slope(q[t],i),g[q[t]])) t--;
q[++t]=i;
}
return mp(g[n],f[n]);
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&s[i]),s[i]+=s[i-1];
int l=-1e16,r=0;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=m) r=mid,res=cur.se+m*mid;
else l=mid;
}
printf("%lld\n",res);
return 0;
}
例7、
题目描述
给定一棵 个点的树,边权 可能为负。
你需要在树上删去恰好 条边,然后加上 条边权为 的边,求新树直径的最大可能值。
注:直径可以仅包含一个点,此时直径长度为零。
数据范围
- 。
时间限制 ,空间限制 。
分析
容易发现加边操作是假的,删边后会形成 个连通块,我们只会把这些连通块的直径串起来。
注意到直径的定义就是最长链,因此题意可以转化为选 条不相交的链,求边权和最大值。
考虑树形 。
表示 子树中选了 条完整的链, 时的最大边权和。
转移 这条边时需要作出的决策:
- ,在 子树内终结这条链。
- ,将 所在链延伸到 ,收益为 。
注:孤立点也可以当作一个连通块,因此需要赋初值 。
时间复杂度 ,可以获得 分。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=3e5+5,inf=1e9;
int k,n,u,v,w;
int sz[maxn],f[maxn][105][3];
vector<pii> g[maxn];
inline void chmax(int &x,int y)
{
if(x<=y) x=y;
}
void dfs(int u,int fa)
{
sz[u]=1,f[u][0][0]=0,f[u][1][2]=0;
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(v==fa) continue;
dfs(v,u);
static int tmp[105][3];
memset(tmp,0xcf,sizeof(tmp));
for(int i=0;i<=sz[u];i++)
for(int j=0;j<=sz[v];j++)
for(int x=0;x<=2;x++)
for(int y=0;y<=2;y++)
{
int a=y==1,b=x==1,val=f[u][i][x]+f[v][j][y];
if(i+j+a<=k) chmax(tmp[i+j+a][x],val);
if(i+j+b<=k&&x!=2&&y!=2) chmax(tmp[i+j+b][x+1],val+w);
}
sz[u]=min(sz[u]+sz[v],k);
memcpy(f[u],tmp,sizeof(tmp));
}
}
int main()
{
scanf("%d%d",&n,&k),k++;
for(int i=1;i<=n-1;i++)
{
scanf("%d%d%d",&u,&v,&w);
g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
}
memset(f,0xcf,sizeof(f));
dfs(1,0);
printf("%d\n",max({f[1][k][0],f[1][k-1][1],f[1][k][2]}));
return 0;
}
凸性的证明还是老套路,能优先选的一定会优先选,因此差分数组不增。
二分斜率 ,每选择一条链会产生 的代价。
这样上面数组的第二维限制消失了,把转移代价放进去即可。
注意斜率绝对值最大为原树直径,可以达到 。
时间复杂度 。
#include<bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=3e5+5,inf=1e18;
int k,n,u,v,w,res;
pii emp,val,f[maxn][3];
vector<pii> g[maxn];
inline pii operator+(pii a,pii b)
{
return mp(a.fi+b.fi,a.se+b.se);
}
inline void chmax(pii &a,pii b)
{
a=max(a,b);
}
void dfs(int u,int fa)
{
f[u][0]=mp(0,0),f[u][1]=mp(-inf,0),f[u][2]=val;
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(v==fa) continue;
dfs(v,u);
static pii tmp[3];
for(int i=0;i<=2;i++) tmp[i]=mp(-inf,0);
for(int i=0;i<=2;i++)
for(int j=0;j<=2;j++)
{
chmax(tmp[i],f[u][i]+f[v][j]+(j==1?val:emp));
if(i!=2&&j!=2) chmax(tmp[i+1],f[u][i]+f[v][j]+mp(w,0)+(i==1?val:emp));
}
for(int i=0;i<=2;i++) f[u][i]=tmp[i];
}
}
pii calc(int x)
{
val=mp(-x,1),dfs(1,0);
return max({f[1][0],f[1][1]+val,f[1][2]});
}
signed main()
{
scanf("%lld%lld",&n,&k),k++;
for(int i=1;i<=n-1;i++)
{
scanf("%lld%lld%lld",&u,&v,&w);
g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
}
int l=-3e11,r=3e11;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.se>=k) l=mid,res=cur.fi+k*mid;
else r=mid;
}
printf("%lld\n",res);
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17372864.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2022-05-04 CF1437G Death DBMS 题解