一般解决问题
你有一个 \(k\),表示最后要变成 \(k\) 个,或者说是选 \(k\)。
形式化地讲,设 \(f(i)\) 表示最后变成 \(i\) 个,或者是选 \(i\) 个的方案。
你一般要求的是 \(f(k)\) 的最大值或者最小值。
问题特征
你最后的 \((x,f(x))\) 是一个凸包。
而凸性意味着:如果我们给每个被选的物品增加一个相同的额外代价/收益 \(\lambda\)(称为惩罚或奖励),那么最优解中选择的物品数量 \(g(\lambda)\) 会随着 \(\lambda\) 单调变化。
为什么,这就是他的原理。
首先我们有一个凸包,然后二分它的斜率,然后就相当于平移这个直线使它的截距最大或者最小,很明显与一个点相切。
求出这个点之后可以根据 \(x\) 继续二分,因为我们求的是 \(x=k\) 的情况。
这个点为 \((x,f(x))\),那么如果我让他每次选惩罚 \(\lambda\),那么它的值就为 \(0\) 了,但是在其它的点上面,惩罚过后都是 \(\leq 0\) 的,那么我当前这个就是要找的截距最大。
于是,我们把每个 \(-\lambda\) 放进去,那么我们所求的最大的(一般用 \(dp\))便是我们要找的 \(x\),基于此二分即可。
细节
这里需要考虑一些边界情况,wqs 二分两个极端是要么只要 \(1/0\) 个,要么全都要,依据这个就行了。
而且,你还需要考虑优化的 \(dp\) 算 \(x\) 的坐标一定要准确。
怎么判断是不是凸包呢?一种可用的方法是代价是凸函数,那么最后就是凸包,或者你可以求导解决。
例题
P4983 忘情
很好玩的题目。
题目概述
把长度为 \(n\) 的序列分成 \(k\) 个部分,每个部分的代价为 \((1+\sum_i a_i)^2\),那么最小的代价是多少呢?
分析
我们来思考一下。
我们注意到(\(\text{attention is all you need}\)),这个代价是单调的,也是凸的,所以可以用 wqs 二分解决。
最后我们求每一个少 \(\lambda\) 的 \(dp\),显然是斜率优化即可。
代码
时间复杂度 \(\mathcal{O}(n\log V)\) 的。
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <algorithm>
#include <cstring>
#include <vector>
#define int long long
#define N 100005
#define sqr(x) (x) * (x)
using namespace std;
int n,m,f[N],a[N],q[N],sum[N],g[N];
double X(int id) {return sum[id];
}
double Y(int id) {return f[id] + sum[id] * sum[id] - 2 * sum[id];
}
double Slope(int fir,int sec) {return (Y(fir) - Y(sec)) / (X(fir) - X(sec));
}
void check(int mid) {memset(f,0x3f,sizeof f),memset(g,0,sizeof g);f[0] = 0;int head = 1,tail = 0;q[++tail] = 0;for (int i = 1;i <= n;i ++) {while(head < tail && Slope(q[head],q[head + 1]) < 2 * sum[i]) head ++;f[i] = f[q[head]] + sqr(sum[i] - sum[q[head]] + 1) + mid;g[i] = g[q[head]] + 1;while(head < tail && Slope(q[tail - 1],q[tail]) > Slope(q[tail],i)) tail --;q[++tail] = i;}
}
signed main(){cin >> n >> m;for (int i = 1;i <= n;i ++) {scanf("%lld",&a[i]);sum[i] = sum[i - 1] + a[i];}int l = 0,r = 1e18,res = 0;while (l <= r) {int mid = l + r >> 1;check(mid);if (g[n] <= m) r = mid - 1,res = mid;//x坐标不在要求的右边,那么我们求的x坐标要尽量靠左,因为如果你算的尽量靠右的话,有可能会被判掉else l = mid + 1;}check(res);cout << f[n] - m * res;return 0;
}
P2619 [国家集训队] Tree I
题目概述
给你一个带权无向连通图(有白边和黑边),恰好用 \(k\) 条白边的最小生成树。
分析
你可以通过打表或者怎么发现:一开始我限制了 \(\leq i\) 条白边,那么对于原本的最小生成树的白边数量 \(p\),如果 \(i\leq p\),那么单调下降,因为有些白边原本就可以替代一些黑边;如果 \(i> p\),那么单调上升,因为有些更优的黑边被你的白边给替代了。
于是我们就能通过此题了。
代码
时间复杂度 \(\mathcal{O}(n\log n\log V)\)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <algorithm>
#include <vector>
#define int long long
#define N 100005
using namespace std;
int n,m,k;
struct node{int u,v,w,col;
}a[N],b[N];
int fa[N];
int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int tj,ans;
void check(int mid) {tj = 0;ans = 0;for (int i = 1;i <= m;i ++) b[i] = a[i];for (int i = 1;i <= m;i ++)if (b[i].col == 0) b[i].w += mid;//更难被选到stable_sort(b + 1,b + 1 + m,[](node x,node y) {if (x.w == y.w) return x.col > y.col;//尽量少用return x.w < y.w;});for (int i = 1;i <= n;i ++) fa[i] = i;for (int i = 1;i <= m;i ++) {int x = b[i].u,y = b[i].v;int xx = find(x),yy = find(y);if (xx != yy) fa[xx] = fa[yy],tj += (b[i].col == 0),ans += b[i].w;}
}
signed main(){cin >> n >> m >> k;int cnt = 0;for (int i = 1;i <= m;i ++) {int u,v,col,w;scanf("%lld%lld%lld%lld",&u,&v,&w,&col);a[i] = {u + 1,v + 1,w,col};cnt += (col == 0);}// for (int i = 0;i <= cnt;i ++) {// for (int j = 1;j <= n;j ++) fa[j] = j;// int choose = 0,ans = 0;// for (int j = 1;j <= m;j ++) {// int x = a[i].u,y = a[i].v;// int xx = find(x),yy = find(y);// if (xx != yy) {// if ((a[i].col == 0 && choose < i) || a[i].col == 1) fa[xx] = fa[yy],cnt += a[i].col == 0,ans += a[i].w;// }// }// printf("%lld %lld\n",i,ans);// }// 从小到大,能用就用白的,但是不能超过限制,所以说前面的代价都是单调下降,到了极点,也就是原本最小生成树的白边个数后,它不得不用更大的白边去替换小的黑边,所以单调上升这是一个凸函数int l = -1e18,r = 1e18,res = -1e18;while(l <= r) {int mid = l + r >> 1;check(mid);if (tj <= k) r = mid - 1,res = mid;else l = mid + 1; }// cout << res << '\n';check(res);cout << ans - k * res;return 0;
}
先暂时更新到这里