WQS二分 学习笔记 + 例题([BZOJ2654]Tree、[联考2018]林克卡特树)

问题类型

形如“恰好取 k 个……时的最优答案(并非具体方案)”的问题。

  • 保证存在合法方案

WQS二分思路

我们先把限制去掉,这时候最终的方案肯定不一定取了 k 个该物品,

假设最优方案该物品个数更小,我们可以每取一个该物品就加一份额外贡献(宏观调控!)来使最终方案的该物品个数增加,若最优方案该物品个数比 k 大,我们可以每取一个该物品就算上一份额外花费来使最终方案的该物品个数减少。

总的来说,就是每取一个该物品就多加一个权值 x,该权值可以为正负,可以发现最优方案中该物品的个数关于 x 一定是单调的,那么就可以二分一个 x,然后在无限制条件下求出最优答案以及最优方案中该物品的个数,使个数刚好大于等于(或小于等于) k。

由于保证存在合法方案,此时相当于得到了刚好选 k 个该物品算上额外权值 x 时的最优答案,这时并不一定我们的最优方案恰好 k 个,但是我们的最优答案和恰好 k 个时的答案是一样的,那么再减去 k*x 就是我们要的答案。

于是我们最终的复杂度就是 O(无限制情况下的复杂度*log)

形象地说

你是一个商店的老板,商店里每种物品有一定的利润,每个月卖出去的物品总数一定,不同种商品销量之间的内在联系千奇百怪,你找不到规律。处于各种考虑,现在你想知道某种商品恰好卖出 k 个时盈利值的最小值,你只能实践。

顾客们很聪明,他们会在满足各种限制的情况下每个月让你的钱包里的钱最少。

如果这种商品正常情况下超过了 k 个,那好办,每买一个该种商品多收一份手续费 ¥x 就行了,手续费进入你的腰包,但是肯定不能算在盈利值里,这样该商品销量会减少。

如果这种商品正常情况下不到 k 个,你可以“行贿”,每买一个该种物品你偷偷给点好处 ¥x ,但是这也不能算在盈利值里,这样该商品销量会增加。

这样一来,你二分一个 x 值,查看盈利值,然后减去你预计的行贿/手续费总数,就可以试验最少的月数得到你想知道的值。

WQS二分就是类似的思路。

板题

题意 & 解法

[BZOJ2654]Tree

给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有need条白色边的生成树,输出此时的总权值,题目保证有解。

通过额外给白边加一份权值 x,调控白边的数量,求出白边数量刚好大于等于need时的最小生成树权值,然后减去 x*need,复杂度 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n)

细节:

  • 求最小生成树时,权值相等的边优先选白边。

CODE

#include<cstdio>
#include<cstring>
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 100005
#define DB double
#define LL long long
#define ENDL putchar('\n')
#define lowbit(x) (-(x) & (x))
LL read() {
	LL f = 1,x = 0;char s = getchar();
	while(s < '0' || s > '9') {if(s=='-')f = -f;s = getchar();}
	while(s >= '0' && s <= '9') {x=x*10+(s-'0');s = getchar();}
	return f * x;
}
const int MOD = 1000000007;
int n,m,i,j,s,o,k,nd;
int U[MAXN],V[MAXN],W[MAXN],cl[MAXN];
int fa[MAXN],b[MAXN];
int findf(int x) {return x==fa[x] ? x:(fa[x] = findf(fa[x]));}
void unionSet(int a,int b) {fa[findf(a)] = findf(b);}
int ADD; LL ANS = 0;
bool cmp(int a,int b) {
	if(W[a]-(1-cl[a])*ADD == W[b]-(1-cl[b])*ADD) return cl[a] < cl[b];
	return W[a]-(1-cl[a])*ADD < W[b]-(1-cl[b])*ADD;
}
int check(int md) {
	ADD = md; ANS = 0;
	for(int i = 1;i <= n;i ++) fa[i] = i;
	for(int i = 1;i <= m;i ++) b[i] = i;
	sort(b + 1,b + 1 + m,cmp);
	int ct = 0;
	for(int i = 1;i <= m;i ++) {
		int s = U[b[i]],o = V[b[i]];
		if(findf(s) != findf(o)) {
			unionSet(s,o);
			ANS += W[b[i]] - (1-cl[b[i]]) * ADD;
			if(cl[b[i]] == 0) ct ++;
		}
	}
	return ct;
}
int main() {
	n = read();m = read();nd = read();
	for(int i = 1;i <= m;i ++) {
		U[i] = read()+1; V[i] = read()+1;
		W[i] = read(); cl[i] = read();
	}
	int l = -100,r = 100,mid;
	while(l < r) {
		mid = ((l + r + 20000) >> 1) - 10000;
		if(check(mid) >= nd) r = mid;
		else l = mid + 1;
	}
	check(l);
	printf("%lld\n",ANS + nd*1ll*l);
	return 0;
}

省选题

题意 & 解法

[八/九省联考2018]林克卡特树

题意很好转化:一棵带正负边权的无根树,恰好选 k+1 条不相交的路径(单点可以算路径),求总的路径权值和最大值。

我们给每条路径一个“报酬” x (可正负),然后跑 dp,令 d p [ i ] [ 0 / 1 / 2 ] dp[i][0/1/2] dp[i][0/1/2] 分别表示点 i i i 不在路径中、点 i i i 为一条路径的上端点、点 i i i 为一条路径的 l c a lca lca 时子树 i i i 的最大权值和满足最大权值时的最多路径条数:

j j j 为此时遍历到的新的 i i i 的儿子,等式右边都是上一个儿子结束时的值

  • d p [ i ] [ 0 ] ( n e w ) = m a x { d p [ i ] [ 0 ] , d p [ i ] [ 0 ] + d p [ j ] [ 0 / 1 / 2 ] } dp[i][0](new) = max\{dp[i][0],dp[i][0]+dp[j][0/1/2] \} dp[i][0](new)=max{dp[i][0],dp[i][0]+dp[j][0/1/2]}
  • d p [ i ] [ 1 ] ( n e w ) = m a x { d p [ i ] [ 1 ] , d p [ i ] [ 1 ] + d p [ j ] [ 0 / 1 / 2 ] , d p [ i ] [ 0 ] + d p [ j ] [ 1 ] + w i , j , d p [ i ] [ 0 ] + d p [ j ] [ 0 / 1 / 2 ] + x } dp[i][1](new) = max\{dp[i][1],dp[i][1]+dp[j][0/1/2],dp[i][0]+dp[j][1]+w_{i,j},dp[i][0]+dp[j][0/1/2]+x \} dp[i][1](new)=max{dp[i][1],dp[i][1]+dp[j][0/1/2],dp[i][0]+dp[j][1]+wi,j,dp[i][0]+dp[j][0/1/2]+x}
    (不变、从前面的儿子连过来、从 j 连过来、自己单独成路径)
  • d p [ i ] [ 2 ] ( n e w ) = m a x { d p [ i ] [ 2 ] , d p [ i ] [ 2 ] + d p [ j ] [ 0 / 1 / 2 ] , d p [ i ] [ 1 ] + d p [ j ] [ 1 ] + w i , j − x } dp[i][2](new) = max\{dp[i][2],dp[i][2]+dp[j][0/1/2],dp[i][1]+dp[j][1]+w_{i,j}-x \} dp[i][2](new)=max{dp[i][2],dp[i][2]+dp[j][0/1/2],dp[i][1]+dp[j][1]+wi,jx}
    (不变、整条路径在先前的子树、从旧儿子连向新儿子)

同时维护最多路径条数,这个就比较简单了,不展开。

最后得到路径条数刚好大于等于 k+1 时的 m a x { d p [ r o o t ] [ 0 / 1 / 2 ] } max\{dp[root][0/1/2]\} max{dp[root][0/1/2]} ,把它减去 (k+1)*x 就是答案了。

CODE

#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 300005
#define DB double
#define LL long long
#define ENDL putchar('\n')
LL read() {
	LL f=1,x=0;char s = getchar();
	while(s < '0' || s > '9') {if(s=='-')f = -f;s = getchar();}
	while(s >= '0' && s <= '9') {x=x*10+(s-'0');s = getchar();}
	return f * x;
}
int n,m,i,j,s,o,k;
struct it{
	LL nm,ct;it(){nm=ct=0;}
	it(LL N,LL C){nm=N;ct=C;}
};
it bing(it a,it b) {
	if(a.nm == b.nm) return a.ct > b.ct ? a:b;
	return a.nm > b.nm ? a:b;
}
LL aw;
vector<it> g[MAXN];
it dp[MAXN][3];
void dfs(int x,int fa) {
	dp[x][1] = dp[x][2] = it((LL)-1e18,0ll);
	dp[x][1] = bing(dp[x][1],it(aw,1));
	dp[x][0] = it(0,0);
	for(int i = 0;i < (int)g[x].size();i ++) {
		int y = g[x][i].nm;
		if(y != fa) {
			LL W = g[x][i].ct;
			dfs(y,x);
			it dp0 = dp[x][0],dp1 = dp[x][1],dp2 = dp[x][2];
			it dpy = bing(dp[y][0],bing(dp[y][1],dp[y][2]));
			dp[x][0] = bing(dp[x][0],it(dp0.nm+dpy.nm,dp0.ct+dpy.ct));
			dp[x][1] = bing(dp[x][1],bing(it(dp1.nm+dpy.nm,dp1.ct+dpy.ct),it(dp0.nm+dp[y][1].nm+W,dp0.ct+dp[y][1].ct)));
			dp[x][1] = bing(dp[x][1],it(dp0.nm+dpy.nm+aw,dp0.ct+dpy.ct+1));
			dp[x][2] = bing(dp[x][2],it(dp2.nm+dpy.nm,dp2.ct+dpy.ct));
			dp[x][2] = bing(dp[x][2],it(dp1.nm+dp[y][1].nm+W-aw,dp1.ct+dp[y][1].ct-1));
		}
	}
	return ;
}
it check(LL ad) {
	aw = ad;
	dfs(1,0);
	return bing(bing(dp[1][0],dp[1][1]),dp[1][2]);
}
int main() {
	n = read();m = read();
	for(int i = 1;i < n;i ++) {
		s = read();o = read();k = read();
		g[s].push_back(it(o,k));
		g[o].push_back(it(s,k));
	}
	LL l = -1e13,r = 1e13,mid;
	while(l < r) {
		mid = ((l + r + (LL)2e17) >> 1) - (LL)1e17;
		it as = check(mid);
		if(as.ct >= m+1) r = mid;
		else l = mid+1;
	}
	it as = check(l);
	printf("%lld\n",as.nm - (m+1)*1ll*l);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值