wqs二分
模型介绍
- 限制选择个物品,最大化收益;不限制条件的情况下,最大值能快速求出来
- 设恰好选择个物品收益为,这是一个上凸壳,即
- 二分斜率,与凸包相切时,截距最大,,可以看成每选一个物品就要多的代价
- 在无限制的情况下求出,最大收益对应的,
x>=k增大mid,否则减少 - 如果
(k,g[k])在上凸壳左半部分(斜率大于等于0),那么答案就一定对 - 如果在右半部分(斜率小于0),需要设定二分边界
- 设下界为,上界根据的最大值设置,如果二分查找的值严格小于,那么说明交易次数的限制并不是瓶颈,而价格才是
- 如果上凸壳上有若干连续的且斜率相等的线段,会对应多个不同的,此时设定在满足最大时,最小,那么即使,,仍然是答案
- 如果是下凸壳(最小化收益)则无需管答案是否合法,因为一定能二分到,此时才是答案
总结:
当为下凸壳时,每个物品添加代价,总答案减去斜率代价;二分上下界都尽可能大
当为上凸壳时,每个物品减少代价,总答案加上斜率代价;考虑左半边和右半边,如果答案在左半边,下界为1
P2619 [国家集训队] Tree I(wqs二分下凸壳板子)
给你一个无向带权连通图,每条边是黑色或白色。求恰好包含条白边的最小生成树
- 假设没有限制,最小值在处取
- 时,那么每次选的白边肯定越来越大,下降速率肯定变慢
- 时,此时白边已经饱和,强行更换白边值会越来越大,并且仍然满足优先选值更小的白边替换,那么增加速率肯定是增大的
- 因此是一个下凸壳
- 如果要求输出方案,同样的边权,如果已经选够k条,则优先选择黑边;否则优先选择白边
void solve(){
int n,m,k;
cin>>n>>m>>k;
vector<array<int,4>> e;
for(int i=0;i<m;i++){
int u,v,w,c;
cin>>u>>v>>w>>c;
e.push_back({w,u,v,c});
}
DSU dsu;
auto check=[&](int mid){
dsu.init(n+1);
for(auto &[w,u,v,c]:e){
if(c==0)w+=mid;
}
ranges::sort(e,[&](auto a,auto b){
if(a[0]!=b[0])return a[0]<b[0];
return a[3]<b[3];
});
int used=0,ans=0;
for(auto [w,u,v,c]:e){
if(dsu.merge(u,v)){
used+=c==0;
ans+=w;
}
}
for(auto &[w,u,v,c]:e){
if(c==0)w-=mid;
}
return PII{used,ans};
};
int l=-100,r=100;
while(l<r){
int mid=l+r+1>>1;
if(check(mid).F>=k)l=mid;
else r=mid-1;
}
auto [used,ans]=check(l);
cout<<ans-l*k<<"\n";
}满足编号的点度数为的情况
void solve(){
int n,m,s,k;
cin>>n>>m>>s>>k;
vector<array<int,4>> e;
for(int i=0;i<m;i++){
int u,v,w,c;
cin>>u>>v>>w;
c=!(u==s||v==s);
e.push_back({w,u,v,c});
}
DSU dsu;
dsu.init(n+1);
for(auto [w,u,v,c]:e){
dsu.merge(u,v);
}
if(dsu.size(s)!=n){
cout<<"Impossible\n";
return;
}
auto check=[&](int mid){
dsu.init(n+1);
for(auto &[w,u,v,c]:e){
if(c==0)w+=mid;
}
ranges::sort(e,[&](auto a,auto b){
if(a[0]!=b[0])return a[0]<b[0];
return a[3]<b[3];
});
int used=0,ans=0;
for(auto [w,u,v,c]:e){
if(dsu.merge(u,v)){
used+=c==0;
ans+=w;
}
}
for(auto &[w,u,v,c]:e){
if(c==0)w-=mid;
}
return PII{used,ans};
};
int l=-1e9,r=1e9;
while(l<r){
int mid=l+r+1>>1;
if(check(mid).F>=k)l=mid;
else r=mid-1;
}
// 优先使用都无法到达k,或者优先不使用超过k,都是不合法的
if(check(-1e9).F<k||check(1e9).F>k){
cout<<"Impossible\n";
return;
}
auto [used,ans]=check(l);
cout<<ans-l*k<<"\n";
}P1484 种树
个点种最多棵树的最大利益,相邻两个位置只能种一个
void chmax(PII &a,PII b){
if(a.F<b.F)a=b;
else if(a.F==b.F&&a.S<b.S)a.S=b.S;
}
PII operator+(PII a,PII b){
return {a.F+b.F,a.S+b.S};
}
void solve(){
int n,k;
cin>>n>>k;
vector<int> a(n+1);
for(int i=1;i<=n;i++)cin>>a[i];
vector f(n+1,vector<PII>(2));
auto check=[&](int mid){
for(int i=0;i<=n;i++){
for(int j=0;j<2;j++)f[i][j]={-INF,0};
}
f[0][0]={0,0};
for(int i=1;i<=n;i++){
chmax(f[i][0],f[i-1][0]);
chmax(f[i][0],f[i-1][1]);
chmax(f[i][1],f[i-1][0]+PII{a[i]-mid,1});
}
return max(f[n][0],f[n][1]);
};
auto [res,cnt]=check(0);
if(cnt<=k){
cout<<res<<"\n";
return;
}
int l=1,r=1e6+10;
while(l<r){
int mid=l+r+1>>1;
if(check(mid).S>=k)l=mid;
else r=mid-1;
}
auto [res1,cnt1]=check(l);
cout<<res1+l*k<<"\n";
}P4767 [IOI 2000] 邮局 加强版
高速公路旁边有一些村庄。高速公路表示为整数轴,每个村庄的位置用单个整数坐标标识。没有两个在同样地方的村庄。两个位置之间的距离是其整数坐标差的绝对值。
邮局将建在一些,但不一定是所有的村庄中。为了建立邮局,应选择他们建造的位置,使每个村庄与其最近的邮局之间的距离总和最小。
你要编写一个程序,已知村庄的位置和邮局的数量,计算每个村庄和最近的邮局之间所有距离的最小可能的总和。
可以预处理出在放一个邮局的最小代价,那么假设没有限制放邮局,有一个朴素的dp做法
二分斜率(理论斜率应该为负,但是只有正斜率才有效,很奇怪)
void chmin(PII &a,PII b){
if(a.F>b.F)swap(a,b);
else if(a.F==b.F)a.S=max(a.S,b.S);
}
void solve(){
int n,k;
cin>>n>>k;
vector<int> a(n+1);
for(int i=1;i<=n;i++)cin>>a[i];
sort(a.begin()+1,a.end());
vector w(n+1,vector<int>(n+1));
for(int l=1;l<=n;l++){
w[l][l]=0;
for(int r=l+1;r<=n;r++)w[l][r]=w[l][r-1]+a[r]-a[(l+r)/2];
}
vector<PII> dp(n+1);
auto check=[&](int mid){
dp.assign(n+1,{INF,0});
dp[0]={0,0};
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++)chmin(dp[i],{dp[j].F+w[j+1][i]+mid,dp[j].S+1});
}
return dp[n];
};
int l=-3e7,r=3e7;
while(l<r){
int mid=l+r+1>>1;
if(check(mid).S>=k)l=mid;
else r=mid-1;
}
auto [ans,cnt]=check(l);
cout<<ans-l*k<<"\n";
}P3354 [IOI 2005] Riv 河流
几乎整个 Byteland 王国都被森林和河流所覆盖。小点的河汇聚到一起,形成了稍大点的河。就这样,所有的河水都汇聚并流进了一条大河,最后这条大河流进了大海。这条大河的入海口处有一个村庄——名叫 Bytetown。
在 Byteland 国,有 个伐木的村庄,这些村庄都座落在河边。目前在 Bytetown,有一个巨大的伐木场,它处理着全国砍下的所有木料。木料被砍下后,顺着河流而被运到 Bytetown 的伐木场。Byteland 的国王决定,为了减少运输木料的费用,再额外地建造 个伐木场。这 个伐木场将被建在其他村庄里。这些伐木场建造后,木料就不用都被送到 Bytetown 了,它们可以在运输过程中第一个碰到的新伐木场被处理。显然,如果伐木场座落的那个村子就不用再付运送木料的费用了。它们可以直接被本村的伐木场处理。
注:所有的河流都不会分叉,形成一棵树,根结点是 Bytetown。
国王的大臣计算出了每个村子每年要产多少木料,你的任务是决定在哪些村子建设伐木场能获得最小的运费。其中运费的计算方法为:每一吨木料每千米 分钱。
- 对于 的数据,保证 。
- 对于 的数据,保证 ,,,,。
- 保证每年所有的木料流到 bytetown 的运费不超过 分。
思路:
- 考虑每多建一个厂,就会增加mid的代价
- 定义表示当前节点为,有伐木场的最近祖先为,当前节点是否建伐木场的最小代价
- 注意是非法状态
- 考虑如何转移,如果当前不建伐木场,那么
- 否则 ,的木头全部到
- 然后考虑当前节点的贡献,显然 (加上运输木头的代价),(建厂的代价)
- 假设根节点有场,答案就是,同时保证相同答案选择建更多的场
- wqs二分,一定可以卡出答案
void chmin(PII &a,PII b){
if(a.F>b.F)swap(a,b);
else if(a.F==b.F)a.S=max(a.S,b.S);
}
void add(PII &a,PII b){
a.F+=b.F;
a.S+=b.S;
}
PII min(PII a,PII b){
if(a.F<b.F||(a.F==b.F&&a.S>b.S))return a;
return b;
}
void solve(){
int n,k;
cin>>n>>k;
vector<PII> adj[n+1];
vector<int> w(n+1);
for(int i=1;i<=n;i++){
cin>>w[i];
int p,d;
cin>>p>>d;
adj[p].emplace_back(i,d);
adj[i].emplace_back(p,d);
}
vector<int> dep(n+1),stk(n+1);
int top=0;
vector f(n+1,vector (n+1,vector<PII>(2)));
function<void(int,int,int,int)> dfs=[&](int u,int fa,int d,int mid){
dep[u]=d;
stk[++top]=u;
for(auto [v,w]:adj[u]){
if(v==fa)continue;
dfs(v,u,d+w,mid);
for(int i=1;i<=top;i++){
int j=stk[i];
if(i!=top){
add(f[u][j][0],min(f[v][j][0],f[v][j][1]));
}
add(f[u][j][1],min(f[v][u][0],f[v][u][1]));
}
}
for(int i=1;i<=top;i++){
int j=stk[i];
if(i!=top)f[u][j][0].F+=w[u]*(dep[u]-dep[j]);
f[u][j][1].F+=mid;
f[u][j][1].S++;
}
top--;
};
auto check=[&](int mid){
for(int i=0;i<=n;i++){
for(int j=0;j<=n;j++){
for(int x=0;x<2;x++)f[i][j][x]={0,0};
}
}
dfs(0,-1,0,mid);
return PII{f[0][0][1].F-mid,f[0][0][1].S-1};
};
int l=1,r=2e9;
while(l<r){
int mid=l+r+1>>1;
if(check(mid).S>=k)l=mid;
else r=mid-1;
}
auto [res,cnt]=check(l);
cout<<res-l*k<<"\n";
}P4072 [SDOI2016] 征途
个数,分成段,求方差最小,输出
思路:
- 首先可以化简式子为 ,为总和,为每段的大小
- 显然是下凸壳
- 直接朴素dp转移即可,复杂度 ,斜率优化为
P4983 忘情
和上题一样,求分割段,最小化 的值
但是此题范围很大,需要斜率优化
,移项得
- ,每个点为
- 特殊注意,优先使用分段最多的状态
void chmin(PII &a,PII b){
if(a.F>b.F)swap(a,b);
else if(a.F==b.F)a.S=max(a.S,b.S);
}
void add(PII &a,PII b){
a.F+=b.F;
a.S+=b.S;
}
PII min(PII a,PII b){
if(a.F<b.F||(a.F==b.F&&a.S>b.S))return a;
return b;
}
void solve(){
int n,m;
cin>>n>>m;
vector<int> a(n+1),sum(n+1);
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
vector<PII> dp(n+1);
vector<int> q(n+1);
auto check=[&](int mid){
for(int i=1;i<=n;i++)dp[i]={INF,0};
dp[0]={0,0};
auto Y=[&](int i){
return dp[i].F+sum[i]*sum[i]-2*sum[i];
};
auto X=[&](int i){
return sum[i];
};
auto cmp=[&](int x,int y){
if(x>y)return 1;
if(x<y)return -1;
return 0;
};
int hh=0,tt=0;
q[0]=0;
for(int i=1;i<=n;i++){
while(hh<tt){
int c=cmp(Y(q[hh+1])-Y(q[hh]),2*sum[i]*(X(q[hh+1])-X(q[hh])));
if(c<0)hh++;
else if(c==0&&dp[q[hh+1]].S>dp[q[hh]].S)hh++;
else break;
}
int j=q[hh];
chmin(dp[i],{dp[j].F+(sum[i]-sum[j]+1)*(sum[i]-sum[j]+1)+mid,dp[j].S+1});
while(hh<tt){
int c=cmp((Y(q[tt])-Y(q[tt-1]))*(X(i)-X(q[tt])),(Y(i)-Y(q[tt]))*(X(q[tt])-X(q[tt-1])));
if(c>0)tt--;
else if(c==0&&dp[i].S>=dp[q[tt]].S)tt--;
else break;
}
q[++tt]=i;
}
return dp[n];
};
int l=-1e16,r=1e16;
while(l<r){
int mid=l+r+1>>1;
if(check(mid).S>=m)l=mid;
else r=mid-1;
}
auto [res,cnt]=check(l);
cout<<res-l*m<<"\n";
}CF321E Ciel and Gondolas
决策单调性,待补。。。
P5308 [COCI 2018/2019 #4] Akvizna
你面临 名参赛者的挑战,最终要将他们全部战胜。
每一轮中,都会淘汰一些选手;你会得到这一轮奖金池中 被淘汰者 除以 这一轮对手总数 比例的奖金。
例如某一轮有 个对手,淘汰了 个,那么你将获得奖金池中 的奖金。
假设每一轮的奖金池均为一元,Mirko 希望通过恰好 轮赢得比赛,那么他最多可能获得多少奖金呢?
你只需要输出答案保留 位小数即可。
对于的数据,。
对于的数据,。
对于的数据,。
本题较卡精度,请留意。
- 上凸壳
- 考虑如何求 ,
- 可以斜率优化
const double eps=1e-14;
int cmp(double x,double y){
if(fabs(x-y)<eps)return 0;
return x<y?-1:1;
}
void chmax(PDI &a,PDI b){
int o=cmp(a.F,b.F);
if(o<0)swap(a,b);
else if(o==0)a.S=max(a.S,b.S);
}
void solve(){
int n,k;
cin>>n>>k;
vector<PDI> dp(n+1);
vector<int> q(n+1);
auto check=[&](double mid){
for(int i=1;i<=n;i++)dp[i]={-INF,0};
dp[0]={0,0};
int hh=0,tt=0;
q[0]=0;
auto Y=[&](int i){
return dp[i].F-1.0*i/(n-i);
};
auto X=[&](int i){
return 1.0/(n-i);
};
for(int i=1;i<=n;i++){
while(hh<tt){
int c=cmp((Y(q[hh+1])-Y(q[hh]))/(X(q[hh+1])-X(q[hh])),-1.0*i);
if(c>0)hh++;
else if(c==0&&dp[q[hh+1]].S>dp[q[hh]].S)hh++;
else break;
}
int j=q[hh];
chmax(dp[i],{dp[j].F+1.0*(i-j)/(n-j)-mid,dp[j].S+1});
while(hh<tt){
int c=cmp((Y(q[tt])-Y(q[tt-1]))/(X(q[tt])-X(q[tt-1])),(Y(i)-Y(q[tt]))/(X(i)-X(q[tt])));
if(c<0)tt--;
else if(c==0&&dp[q[tt]].S<dp[i].S)tt--;
else break;
}
q[++tt]=i;
}
return dp[n];
};
double l=0,r=1e18;
while(l+eps<r){
double mid=(l+r)/2;
if(check(mid).S>=k)l=mid;
else r=mid;
}
auto [res,cnt]=check(l);
cout<<fixed<<setprecision(9);
cout<<res+l*k<<"\n";
}