都是要退役的人了,不知道为啥想学,就看了看,做了两个板子,写个笔记。
例一 P2619 [国家集训队]Tree I
很早以前就做过了,但根本就搞不明白为什么要去调整白边的权值二分答案。
简要题意
有一个 n n n 个点 m m m条边的无向图,其中每一条边都有权值、颜色,问恰好有 n e e d need need 条白边的最小生成树的答案。 ( n < = 5 e 4 , n e e d < = m < = 1 e 5 ) (n<=5e4,need<=m<=1e5) (n<=5e4,need<=m<=1e5)
题解
设
g
(
x
)
g(x)
g(x) 表示恰好选
x
x
x 条白边的最小生成树的答案,那么答案就是
g
(
n
e
e
d
)
g(need)
g(need) 。但是直接求解显然复杂 (我不会) ,考虑
g
g
g 函数,我们就是要求这个函数
x
=
n
e
e
d
x=need
x=need 时的
y
y
y 值。发现
g
g
g 是凹函数,也就是这个函数的斜率
k
k
k 单增,考虑去二分
k
k
k 值,找到斜率
k
k
k 切的点的位置,即满足
g
(
x
)
=
k
x
+
b
g(x)=kx+b
g(x)=kx+b ,发现一定是让截距
b
b
b 取
m
i
n
min
min 的位置
x
x
x 。又因为
b
=
g
(
x
)
−
k
x
b=g(x)-kx
b=g(x)−kx ,即问题变成找
x
x
x 让
g
(
x
)
−
k
x
g(x)-kx
g(x)−kx 最小,这道题中
g
(
x
)
g(x)
g(x) 的
x
x
x 的含义就是选
x
x
x 条白边时的总边权的最小,由于每选一条白边影响都是
−
k
-k
−k 是一样的,那把每条白边的权值
−
k
-k
−k 求最小生成树就能知道
x
x
x 的取值了,即此时选择白边的数量。然后比较
x
x
x 与
n
e
e
d
need
need 就能知道当前斜率
k
k
k 偏大/偏小,最后二分出
k
k
k 再跑遍最小生成树就能得到答案(注意要
+
k
∗
n
e
e
d
+k*need
+k∗need 补充没有考虑的)。注意题目条件
a
b
s
(
边
权
)
<
=
100
abs(边权)<=100
abs(边权)<=100 ,斜率就在
−
100
-100
−100 到
100
100
100 之间二分就可以了。
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10,M=1e5+10,K=110;
int n,m,x,y,z,need,fa[N];
struct Edge{
int x,y,v;
inline bool operator < (const Edge &b)const{
return v<b.v;
}
}e1[M],e2[M]; int cnt1,cnt2;
inline int getfa(const int x){
return x==fa[x]?x:fa[x]=getfa(fa[x]);
}
inline bool merge(const int x,const int y){
int fx=getfa(x),fy=getfa(y); if(fx==fy) return 0;
fa[fx]=fy; return 1;
}
inline bool check(const int k){
for(int i=0;i<n;i++) fa[i]=i; int use=0;
int i1=1,i2=1; while(i1<=cnt1&&i2<=cnt2)
(e1[i1].v<e2[i2].v-k)?(merge(e1[i1].x,e1[i1].y),++i1):(use+=merge(e2[i2].x,e2[i2].y),++i2);
while(i2<=cnt2) use+=merge(e2[i2].x,e2[i2].y),++i2;
return use<need;
}
#define gc getchar()
#define in read()
inline int read(){
int f=1,k=0; char cp=gc; while(cp!='-'&&(cp<'0'||cp>'9')) cp=gc; if(cp=='-') cp=gc;
while(cp>='0'&&cp<='9') k=(k<<3)+(k<<1)+cp-48,cp=gc; return f*k;
}
int main(){
n=in,m=in,need=in;
for(int i=1;i<=m;i++){ x=in,y=in,z=in;
if(in) e1[++cnt1]=Edge{x,y,z};
else e2[++cnt2]=Edge{x,y,z};
} sort(e1+1,e1+cnt1+1),sort(e2+1,e2+cnt2+1);
int l=-K,r=K; while(l<r){ int mid=(l+r)>>1;
if(check(mid)) l=mid+1; else r=mid;
} int k=l;
for(int i=0;i<n;i++) fa[i]=i; int ans=0;
int i1=1,i2=1; while(i1<=cnt1&&i2<=cnt2)
(e1[i1].v<e2[i2].v-k)?(ans+=e1[i1].v*merge(e1[i1].x,e1[i1].y),++i1):(ans+=(e2[i2].v-k)*merge(e2[i2].x,e2[i2].y),++i2);
while(i2<=cnt2) ans+=(e2[i2].v-k)*merge(e2[i2].x,e2[i2].y),++i2;
while(i1<=cnt1) ans+=e1[i1].v*merge(e1[i1].x,e1[i1].y),++i1;
printf("%d\n",ans+k*need);
return 0;
}
例二 CF739E Gosha is hunting
简要题意 (由概率期望魔性翻译而来)
有n个物品,分别有两种价值 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an 和 b 1 , b 2 , . . . , b n b_1,b_2,...,b_n b1,b2,...,bn ,给定两种价值分别可以选的个数为 A , B A,B A,B ,可以任选并收益对应的价值,但是对于同一个 i i i ,如果两种价值都选了则收益减少 a i × b i a_i\times b_i ai×bi ,求最大收益。 ( A , B < = n < = 2000 ) (A,B<=n<=2000) (A,B<=n<=2000)
题解
n 3 n^3 n3 做法显然, f [ k ] [ i ] [ j ] f[k][i][j] f[k][i][j] 表示前 k k k 个选了 i i i个价值一、 j j j 个价值二的最大收益。考虑优化,如果固定了 i i i,那对于 f [ n ] [ i ] [ x ] f[n][i][x] f[n][i][x] 来说一定是关于 x x x的一个凸函数,就可以用 w q s wqs wqs 二分求出价值一个数 i i i 对应应选出价值二个数 j j j 的数量和其对于的最大收益,复杂度变成 O ( n 2 l o g k ) O(n^2logk) O(n2logk) 。考虑价值一也满足这个性质,所以变成 w q s wqs wqs 二分套 w q s wqs wqs 二分,复杂度优化到 O ( n l o g 2 k ) O(nlog^2k) O(nlog2k) ,可以通过本题。
#include<bits/stdc++.h>
using namespace std;
const int N=2010;
const double eps=1e-8;
int n,a,b,numa[N],numb[N]; double pa[N],pb[N],f[N],ka,kb;
inline void check_b(const double ka,const double kb){
memset(numa,0,sizeof(numa)),memset(numb,0,sizeof(numb)),memset(f,0,sizeof(f));
for(int i=1;i<=n;i++){
f[i]=f[i-1],numa[i]=numa[i-1],numb[i]=numb[i-1];
if(f[i]<f[i-1]+pa[i]-ka) f[i]=f[i-1]+pa[i]-ka,numa[i]=numa[i-1]+1,numb[i]=numb[i-1];
if(f[i]<f[i-1]+pb[i]-kb) f[i]=f[i-1]+pb[i]-kb,numb[i]=numb[i-1]+1,numa[i]=numa[i-1];
if(f[i]<f[i-1]+pa[i]+pb[i]-pa[i]*pb[i]-ka-kb) //注意ka,kb的实际含义是选一次a,b所需要的代价
f[i]=f[i-1]+pa[i]+pb[i]-pa[i]*pb[i]-ka-kb,numa[i]=numa[i-1]+1,numb[i]=numb[i-1]+1;
}
}
inline bool check_a(const double ka){
double l=0,r=1; for(int i=1;i<=50;i++){
double mid=(l+r)/2.0; check_b(ka,mid);
if(numb[n]<b) r=mid-eps; else l=mid;
} kb=l; check_b(ka,kb); return numa[n]<a;
}
#define gc getchar()
#define in read()
inline int read(){
int f=1,k=0; char cp=gc; while(cp!='-'&&(cp<'0'||cp>'9')) cp=gc; if(cp=='-') cp=gc;
while(cp>='0'&&cp<='9') k=(k<<3)+(k<<1)+cp-48,cp=gc; return f*k;
}
int main(){
n=in,a=in,b=in;
for(int i=1;i<=n;i++) scanf("%lf",&pa[i]);
for(int i=1;i<=n;i++) scanf("%lf",&pb[i]);
double l=0,r=1; for(int i=1;i<=50;i++){ //满足精度就行
double mid=(l+r)/2.0; if(check_a(mid)) r=mid-eps; else l=mid;
} ka=l; check_a(ka); printf("%.7lf\n",f[n]+ka*a+kb*b); //yyb说最好多保留3位
return 0;
}
小结
使用 w q s wqs wqs 二分的几个条件:
- 题目限定选择个数求最值,且选择与最值关系复杂,或求解复杂度不能承受。
- 关于选择个数的最值函数是凸或凹的(斜率单降或增)。(在上面两个例题中就是多选的对答案越来越没意义了,甚至还有副作用。当然另一种就是越来越有意义,答案增长越来越快)
- 能够在没有数量限制的条件下快速求出最优解和其期望的选择个数。