二分答案
二分答案
对于一个需要求最优解的问题
当直接求解问题较难时,可以通过二分答案对问题进行转化
适用范围:答案具有单调性
即答案为 x 时可行则答案小于等于 x 时都可行;
答案为 x 时不可行则答案大于等于 x 时都不可行.
二分答案将最优性问题转化为可行性问题
二分
二分有诸多写法,不同方法各有特色
对于每种二分写法,都有需要注意的地方
稍有不慎,便会出现死循环或所求答案不正确等情况
这里介绍其中一种二分的写法
特点
该二分方法保证答案时刻处于区间\([l,r]\)内
且循环结束时,有 \(l=r\)
问题引入
给定一个 非严格单调递增或单调递减 的 \(01\) 序列
利用二分求解该 \(01\) 序列中 \(0\) 和 \(1\) 交界的两个位置下标
以下代码为该二分答案的模板:
bool check(int x) { return a[x]; }
void solve() {
int l = 1, r = Mx;
while(l < r) {
int mid = (____①____) >> 1;
if(check(mid)) ____②____;
else ____③____;
}
printf("%d\n", l); // 此时 l 和 r 相同
}
/*
①:选填 l + r 或 l + r + 1
②和③:选填 l = mid 或 l = mid ± 1
或 r = mid 或 r = mid ± 1
*/
请利用上面的二分模板分别解决如下四类问题:
- 0000011111 :单调递增 \(01\) 序列 中查找末个 \(0\) 的位置
- 0000011111 :单调递增 \(01\) 序列 中查找首个 \(1\) 的位置
- 1111100000 :单调递减 \(01\) 序列 中查找末个 \(1\) 的位置
- 1111100000 :单调递减 \(01\) 序列 中查找首个 \(0\) 的位置
解决方法
#include <cstdio>
int Mx = 10;
//0000011111
bool check1(int x) { return x >= 6 && x <= 10; }
void solve1() {
int l = 1, r = Mx;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(check1(mid)) r = mid - 1;
else l = mid;
}
printf("%d\n", l); // 此时 l 和 r 相同
}
//0000011111
bool check2(int x) { return x >= 6 && x <= 10; }
void solve2() {
int l = 1, r = Mx;
while(l < r) {
int mid = (l + r) >> 1;
if(check2(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l); // 此时 l 和 r 相同
}
//1111100000
bool check3(int x) { return x >= 1 && x <= 5; }
void solve3() {
int l = 1, r = Mx;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(check3(mid)) l = mid;
else r = mid - 1;
}
printf("%d\n", l); // 此时 l 和 r 相同
}
//1111100000
bool check4(int x) { return x >= 1 && x <= 5; }
void solve4() {
int l = 1, r = Mx;
while(l < r) {
int mid = (l + r) >> 1;
if(check4(mid)) l = mid + 1;
else r = mid;
}
printf("%d\n", l); // 此时 l 和 r 相同
}
int main() {
solve1();
solve2();
solve3();
solve4();
return 0;
}
总结
-
②和③可根据序列特点判断所填写的内容
-
①可根据②和③中是否出现过 \(-1\) 判断是否需要 \(+1\)
-
若序列中不存在符合条件的下标(如序列全为 \(0\) 或全为 \(1\))
则需最后对所求答案进行 \(check\)
例题
P2678 [NOIP2015 提高组] 跳石头
给定 \(n\) 块石头,可以移走其中至多 \(m\) 块(不能移走第一块和最后一块)
最大化 相邻石头的距离最小值
\(0\leq m\leq n\leq 50000\)
题解:
直接对问题求解较难,“最大化最小值”一般考虑二分答案
考虑是否能够转化为问题判定
当 相邻石头的距离最小值(x) 越大时,所需移走的石块(y)就越多
可以将问题转化为:当 \(x\) 确定时,\(y\) 是否 \(\leq m\)
答案具有单调性,构成 \(01\) 序列:111110000000
答案即为最后一个 \(1\) 的位置,二分答案即可
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
int read() {
int x = 0, f = 1; char ch;
while(! isdigit(ch = getchar())) (ch == '-') && (f = -f);
for(x = ch ^ 48; isdigit(ch = getchar()); x = (x << 3) + (x << 1) + (ch ^ 48));
return x * f;
}
template <class T> T Max(T a, T b) { return a > b ? a : b; }
template <class T> T Min(T a, T b) { return a < b ? a : b; }
int L, n, m, d[N];
bool check(int x) { //相邻石块最小值 >= x 11111 1 0000000000
int cnt = 0;
for(int i = 1, lst = 0; i <= n; ++ i) {
if(d[i] - d[lst] >= x) lst = i;
else cnt ++;
}
return cnt <= m;
}
int main() {
L = read(); n = read(); m = read();
for(int i = 1; i <= n; ++ i) d[i] = read();
d[0] = 0; d[++ n] = L;
int l = 0, r = L;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
printf("%d\n", l);
return 0;
}
神光
题目描述
亮亮成功地念出了咒语,石门缓缓地自动移开,一道道绚丽的神光从城堡内激射而出。
亮亮好奇而又兴奋地走入了城堡中,迎面有一座极长的魔法阵。
魔法阵可以看作一条直线,它被均匀地分成了 1 000 000 000 个位置,一个位置可以看成是一个格子。
有些位置上筑有法坛,一共 \(N\) 座。亮亮只有破了眼前的魔法阵,才能继续前进,而欲破法阵,必须毁掉所有的法坛。
亮亮身前有两根法杖:一根颜色血红,能发红色神光,光芒可以笼罩连续 \(L\) 个位置,并摧毁这 \(L\) 个位置上所有的法坛,最多使用 \(R\) 次;另一根颜色碧绿,能发绿色神光,光芒可以笼罩连续 \(2L\) 个位置,并摧毁这 \(2L\) 个位置上所有的法坛,最多使用 \(G\) 次。
法杖的神奇之处在于,\(L\) 的值必须由亮亮事先设定好,并且一经设定,便无法更改。亮亮需要在规定的次数下摧毁所有法坛,并且使得 \(L\) 最小。
输入格式
第一行三个整数 \(N, R, G\)。
第 \(i (2\leq i\leq n+1)\) 行一个整数 \(A_i\),表示第 \(i\) 座法坛的位置。
输出格式
只有一个整数,表示 \(L\) 的最小值。
样例输入
3 1 1 22 17样例输出
4样例解释
亮亮将 \(L\) 设为 \(4\),并用红色神光笼罩 \(21-24\) 位置,用绿色神光笼罩 \(1-8\) 位置。
数据规模
对于 50%的数据, \(N \leq 100\);
对于 100%的数据, \(1 \leq N \leq 2000, 1 \leq R, G, A_i \leq 1,000,000,000\)。
题解:
首先我们注意到,当 \(R\) 和 \(G\) 的大小超过了 \(N\) 时,\(L\) 的最小值就是 \(1\),
因此,我们只 需要考虑 \(R\) 和 \(G\) 小于 \(N\) 的情况,于是 \(R,G\) 的规模就降到了 \(2000\) 以内
显然要采用二分答案的方法。
那么问题转化为,判断给定的 L 能否摧毁所有法坛,我 们采用动态规划方法。
首先将法坛的位置按照从小到大进行排序。
令 \(dp[i][j]\)表示,在用了 \(i\) 次红光,\(j\) 次绿光的情况下,最多从第一座法坛开始,一直摧毁到第几座法坛。
那么状态转 移方程即为 \(dp[i][j] = max ( P[dp[i-1][j] + 1], Q[dp[i][j-1] + 1] )\)
其中 \(P[k]\) 表示使用一次红光, 能从第 \(k\) 座法坛向右(正向为右)连续摧毁到第几座,\(Q[k]\) 表示使用一次绿光,能从第 \(k\) 座 法坛向右连续摧毁到第几座。
\(P\) 和 \(Q\) 数组可以通过预处理得到
最终,我们只要判断 \(dp[R][G]\) 的值是否为 \(N\) 即可
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e3 + 5;
int read() {
int x = 0, f = 1; char ch = getchar();
while(ch < '0'||ch > '9') f = (ch=='-')?-1:1, ch = getchar();
while(ch >= '0'&&ch <= '9') x=(x<<3)+(x<<1)+ch-'0', ch = getchar();
return x * f;
}
int n, r, g, a[N], f[N][N], p[N], q[N];
bool check(int x) {
memset(f, 0, sizeof(f));
memset(p, 0, sizeof(p));
memset(q, 0, sizeof(q));
for(int i = 1;i <= n;i ++) {
for(int j = i;j <= n;j ++) {
if(a[j] - a[i] + 1 <= x) p[i] = j;
if(a[j] - a[i] + 1 <= x * 2) q[i] = j;
}
}
p[n+1] = q[n+1] = n;
for(int i = 0;i <= r;i ++) {
for(int j = 0;j <= g;j ++) {
if(i > 0) f[i][j] = max(f[i][j], p[f[i-1][j]+1]);
if(j > 0) f[i][j] = max(f[i][j], q[f[i][j-1]+1]);
}
}
return f[r][g] == n;
}
int main() {
n = read(); r = read(); g = read();
if(n <= r + g) { printf("1\n"); return 0; }
for(int i = 1;i <= n;i ++) a[i] = read();
sort(a + 1, a + n + 1);
int l = 1, r = 1e9 + 5;
while(l < r) {
int mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l);
return 0;
}
CF1100E Andrew and Taxi
题目描述
给定一个有向图,改变其中某些边的方向,它将成为一个有向无环图
现在求一个改变边方向的方案,使得所选边边权的最大值最小
输入格式
点数n,边数m,接下来是m条有向边
输出格式
第一行输出两个值,一个所选边权最小值和边数k
接下来一行k个编号,表示那些边需要反向
输入样例
5 6 2 1 1 5 2 6 2 3 2 3 4 3 4 5 5 1 5 4 5 7 2 1 5 3 2 3 1 3 3 2 4 1 4 3 5 5 4 1 1 5 3输出样例
2 2 1 3 3 3 3 4 7
题解:
每次取到一个 \(mid\) ,只保留长度大于 \(mid\) 的边
①对于比 \(mid\) 大的边,不能改变方向,于是直接加入图中
②然后只需看看有没有环就行了,因为比 \(mid\) 小的边我们可以任意更改
\(dfs\) 判环,若有环,说明 \(ans>mid\) ,否则 \(ans\leq mid\)
可以用拓扑排序做
因为它只让最大值最小,并没有说改变边的数量最小,所以小的边随便改
考虑输出方案
我们在拓扑排序的时候记一下每个点的拓扑序
考虑一条边 \(x\) 到 \(y\),如果 \(x\) 的拓扑序大于 \(y\),显然可能成环(不是一定成环)
但是如果 \(x\) 的拓扑序小于 \(y\),一定不会成环
题目有不限制改边数量,我们就将其反向即可
#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e5 + 5;
int n, m, maxl = 0, indeg[N], b[N], t;
int head[N], edge[N], leng[N], nxt[N], from[N];
vector<int> ans;
bool v[N], w[N];
queue<int> q;
void add(int x, int y, int z, int i) {
edge[i] = y;
leng[i] = z;
nxt[i] = head[x];
head[x] = i;
from[i] = x;
}
bool dfs(int x, int now) {
v[x] = 1; w[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int y = edge[i], z = leng[i];
if (z <= now) continue;
if (w[y] || !dfs(y, now)) return 0;
}
w[x] = 0;
return 1;
}
inline bool check(int now) {
memset(v, 0, sizeof(v));
memset(w, 0, sizeof(w));
for (int i = 1; i <= n; i++)
if (!v[i] && !dfs(i, now)) return 0;
return 1;
}
void topsort(int now) {
for (int i = 1; i <= n; i++)
if (!indeg[i]) q.push(i);
while (q.size()) {
int x = q.front();
q.pop();
b[x] = ++t;
for (int i = head[x]; i; i = nxt[i]) {
int y = edge[i], z = leng[i];
if (z > now && !--indeg[y]) q.push(y);
}
}
}
int work(int now) {
for (int i = 1; i <= m; i++) {
int y = edge[i], z = leng[i];
if (z > now) ++indeg[y];
}
topsort(now);
for (int i = 1; i <= n; i++)
if (!b[i]) b[i] = ++t;
for (int i = 1; i <= m; i++) {
int x = from[i], y = edge[i], z = leng[i];
if (z <= now && b[x] > b[y]) ans.push_back(i);
}
return ans.size();
}
int main() {
cin >> n >> m;
for (int i = 1, x, y, z; i <= m; i++) {
scanf("%d%d%d", &x, &y, &z);
add(x, y, z, i); maxl = max(maxl, z);
}
int l = 0, r = maxl;
while (l < r) {
int mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << l << " " << work(l) << endl;
for (int i = 0; i < ans.size(); i++) printf("%d ", ans[i]);
return 0;
}

浙公网安备 33010602011771号