二分答案

二分答案

对于一个需要求最优解的问题

当直接求解问题较难时,可以通过二分答案对问题进行转化

适用范围:答案具有单调性

即答案为 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;
}
posted @ 2022-05-23 17:32  计网君  阅读(129)  评论(0)    收藏  举报