YbtOJ 「基础算法」 第3章 二分算法
二分
A. 【例题1】数列分段
[题目描述]
对于给定的一个长度为N的正整数数列 \(A_{1\sim N}\),现要将其分成 \(M\)(\(M\leq N\))段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 \(4\ 2\ 4\ 5\ 1\) 要分成 \(3\) 段。
将其如下分段:
第一段和为 \(6\),第 \(2\) 段和为 \(9\),第 \(3\) 段和为 \(1\),和最大值为 \(9\)。
将其如下分段:
第一段和为 \(4\),第 \(2\) 段和为 \(6\),第 \(3\) 段和为 \(6\),和最大值为 \(6\)。
并且无论如何分段,最大值不会小于 \(6\)。
所以可以得到要将数列 \(4\ 2\ 4\ 5\ 1\) 要分成 \(3\) 段,每段和的最大值最小为 \(6\)。
[输入格式]
第 \(1\) 行包含两个正整数 \(N,M\)。
第 \(2\) 行包含 \(N\) 个空格隔开的非负整数 \(A_i\),含义如题目所述。
[输出格式]
一个正整数,即每段和最大值最小为多少。
[算法分析]
看到最大值最小 显然想到二分 二分的一般策略是二分一个答案 用check函数来判断正确性 再决定缩小上界还是下界
上界初值显然是所有数的和(只分一段)
下界初值就是所有数的最大值(每一个数都分一段)
最后的答案就是l
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , a[N] , l , r , ans;
int check ( int mid )
{
int cnt = 1 , sum = 0;
for ( int i = 1 ; i <= n ; i ++ )
{
if ( sum + a[i] > mid ) cnt ++ , sum = 0;
sum += a[i];
}
return m < cnt;//<如果为真 则不可行 否则可行
}
signed main ()
{
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , l = max ( l , a[i] ) , r += a[i];
while ( l <= r )
{
int mid = l + r >> 1;
if ( check ( mid ) ) l = mid + 1;
else r = mid - 1;
}
printf ( "%d" , l );
return 0;
}
B. 【例题2】防具布置
[题目描述]
现在有\(n\)组防具。 我们可以认为防线是一维的,那么每一组防具都分布在防线的某一段上,并且同一组防具是等距离排列的。 也就是说,我们可以用三个整数 和 来描述一组防具,即这一组防具布置在防线的\(S,S+D,S+2D,...,S+k D(k\in\mathbf{Z},S+k D\le E,S+(k+1)D>E)\)位置上.若一个位置上的防具数量为奇数,则我们称这个位置有破绽,但是整个防线上有且仅有一个位置有破绽或根本没有破绽。请你求出破绽的位置,或是确定防线没有破绽。
[输入格式]
第一行是一个整数\(T\),表示有\(T\)组互相独立的测试数据。
每组数据的第一行是一个整数\(N\)。
之后\(N\)行,每行三个整数\(s_i,e_i,d_i\),代表第\(i\)组防具的三个参数,数据用空格隔开。
[输出格式]
对于每组测试数据,如果防线没有破绽,输出一行 There's no weakness.。
否则在一行内输出两个空格分隔的整数\(P\)和\(C\),表示在位置\(P\)有\(C\)个防具。当然\(C\)应该是奇数。
[算法分析]
开long long很有必要
首先 因为保证了只有一个分界点 那么我们二分这个分界点
如果1-mid段上的防具数量之和是偶数 那么收缩下界 如果是奇数 说明1-mid这一段中有那个分界点 收缩上界
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
const int N = 1e6 + 5;
const int inf = INT_MAX;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , l , r;
struct node { int s , e , d; } a[N];
//s是防具起始位置,e是防具结束位置,d是防具间间隔,
int check ( int mid )//获取到1-mid区间内防具数量之和
{
int ans = 0;
for ( int i = 1 ; i <= n ; i ++ )
{
if ( a[i].s <= mid ) ans += ( ( min ( a[i].e , mid ) - a[i].s ) / a[i].d ) + 1;
else break;
}
return ans;
}
signed main ()
{
int T = read();
while ( T -- )
{
n = read();
for ( int i = 1 ; i <= n ; i ++ )
a[i].s = read() , a[i].e = read() , a[i].d = read();
l = 0 , r = inf;
if ( check(inf) % 2 == 0 ) { printf ( "There's no weakness.\n" ); continue; }
sort ( a + 1 , a + n + 1 , [](const node a , const node b) { return a.s < b.s; } );
while ( l <= r )
{
int mid = l + r >> 1;
if ( check(mid) % 2 == 0 ) l = mid + 1;
else r = mid - 1;
}
printf ( "%lld %lld\n" , l , check(l) - check(l-1) );
}
return 0;
}
C. 【例题3】最大均值
[题目描述]
给定正整数序列\(A\),求一个平均数最大的,长度不小于\(L\)的(连续的)子段。
[输入格式]
第一行两个整数\(N\)和\(L\)。
接下来\(N\)行,每行输入一个正整数\(A_i\)。
[输出格式]
输出一个整数,表示平均值的最大值乘以\(1000\)再向下取整之后得到的结果。
[算法分析]
考虑二分平均值答案
对于每一次查询 先将所有数组减去这个平均值再 查询(1)--(k)(1<=k<=i-m)中所有前缀和的最小值用来转移 每一次用这个点的前缀和减去这个最小值判断最大子段 只要有一个子段大于等于0 则考虑收缩下界 如果所有子段都不能大于这个mid平均值 则考虑收缩上界
实数域二分 考虑向下取整 所以用r作为答案
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid ((l+r)/2)
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;
const double eps = 1e-6;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , L;
double l = inf , r = -inf , a[N] , b[N] , sum[N];
int check ( double x )
{
for ( int i = 1 ; i <= n ; i ++ ) b[i] = a[i] - x;
for ( int i = 1 ; i <= n ; i ++ ) sum[i] = sum[i-1] + b[i];
double ans = -inf , minn = inf;
for ( int i = L ; i <= n ; i ++ )
{
minn = min ( minn , sum[i-L] );
ans = max ( ans , sum[i] - minn );
}
return ans > 0;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , L = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , l = min ( l , a[i] ) , r = max ( r , a[i] );
while ( r - l >= eps )//二分这个平均值
{
if ( check(mid) ) l = mid;
else r = mid;
}
cout << (int)floor(r*1000) << endl;
return 0;
}
D. 1.喂养宠物
[题目描述]
兔兔是可爱的动物,小明想拥有一些。
宠物店提供\(n\)个兔兔,编号为\(1-n\),小明很喜欢,所以他想拥有得越多越好。初始,每个兔兔每天需要固定量的食物。但是,如果它看见别的兔兔也在吃东西,他会觉得饥饿而吃更多的东西。一个兔兔每多一个同食者需要增加一个固定量的食物。
\(hunger_i\)表示第\(i\)个兔兔单独进食所需要的食物。 表示第\(greed_i\)个兔兔在每多一个同食者的情况下增加的食物量。小明每天最多可以供应\(totalfood\)量食物,那么他最多可以养多少只兔兔。
[输入格式]
第一行两个整数\(n\)和\(totalfood\)。
第二行\(n\)个整数,第\(i\)个为\(hunger_i\)。
第三行\(n\)个整数,第\(i\)个为\(greed_i\)。
[输出格式]
一个整数,表示小明最多可以养多少只兔兔。
[算法分析]
二分一个兔兔最大值 每一次统计需要的食物总量 并与\(totalfood\)比较
[代码实现]
#include<bits/stdc++.h>
using namespace std;
#define inl inline
#define mid ((l+r)>>1)
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , k;
struct node { int hun , gre , res; }a[N];
int check ( int x )
{
int ans = 0;
for ( int i = 1 ; i <= n ; i ++ ) a[i].res = a[i].hun + a[i].gre * ( x - 1 );
sort ( a + 1 , a + n + 1 , [](const node &x , const node &y) { return x.res < y.res; } );
for ( int i = 1 ; i <= x ; i ++ ) ans += a[i].res;
return ans <= k;
}
signed main ()
{
n = read() , k = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i].hun = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i].gre = read();
int l = 0 , r = n;
while ( l <= r )
{
if ( check(mid) ) l = mid + 1;
else r = mid - 1;
}
printf ( "%d" , r );
return 0;
}
E. 2.最小时间
[题目描述]
有\(n\)个物品,第\(i\)个物品有两个属性\(k_i,b_i\),表示它在时刻\(x\)的价值为\(k_i*x+b_i\)。
当前处于时刻\(0\),你可以选择不超过\(m\)个物品,使得存在非负整数时刻\(t\),你选择的所有物品的总价值大于等于\(S\)。
给出\(S\),求\(t\)的最小值。
[输入格式]
第一行三个整数\(n,m,S\)。
接下来\(n\)行,第\(i\)行两个整数\(k_i,b_i\)。
[输出格式]
一行一个整数表示答案。
[算法分析]
我们二分一个最大距离 \(1-r\)区间内是符合条件的 \(l-n\)区间内是不符合条件的
[代码实现]
#include<bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
#define mid ((l+r)>>1)
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , s;
struct node { int k , b , tot; } a[N];
//对于每一个时间 排序一下 选出前m个价值最大的值是绝对不劣的
int check ( int x )
{
for ( int i = 1 ; i <= n ; i ++ ) a[i].tot = a[i].k * x + a[i].b;
nth_element ( a + 1 , a + m + 1 , a + n + 1 , [](const node &a , const node &b) { return a.tot > b.tot; } );
int ans = 0;
for ( int i = 1 ; i <= m ; i ++ )
{
if ( a[i].tot < 0 ) continue;
ans += a[i].tot;
if ( s <= ans ) return true;
}
return s <= ans;
}
signed main ()
{
n = read() , m = read() , s = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i].k = read() , a[i].b = read();
if ( check (0) ) { cout << "0" << endl; return 0; }
int l = 0 , r = 1e9 + 5;
while ( l <= r )
{
if ( check (mid) ) r = mid - 1;
else l = mid + 1;
}
printf ( "%lld" , l );
return 0;
}
F. 3.攻击法坛
[题目描述]
有一个魔法阵。
魔法阵可以看作一条直线,有些位置上筑有法坛,一共有\(n\)座。
你现在要摧毁魔法阵上的\(n\)座法坛,你有两根法杖:一根可以笼罩连续\(L\)个位置,并摧毁这\(L\)个位置上所有的法坛,最多使用\(R\)次;另一根可以笼罩连续\(2L\)个位置,并摧毁这\(2L\)个位置上所有的法坛,最多使用\(G\)次。
法杖的神奇之处在于,\(L\)的值必须由你事先设好,并且一经设定,便无法更改。亮亮需要在规定的次数下摧毁所有法坛,并且使得\(L\)最小。
[输入格式]
第一行三个整数\(n,R,G\)。
接下来\(n\)行,每行一个整数\(a_i\),表示第\(i\)座法坛的位置。
[输出格式]
只有一个整数,表示\(L\)的最小值。
[算法分析]
\(k1[i]\) 表示在第 i 个位置用普通法杖最远可以覆盖到的最远点的下标
\(k2[i]\) 就是用高级法杖最远可以覆盖到的最远点的下标
\(f[i][j]\) 表示用 i 个普通法杖和 j 个高级法杖能到的(所有特殊点都覆盖)的最远点
\(q1,q2\)为法杖从\(a[i]\)出发最远能覆盖的位置
之后二分一个\(L\) 计算能覆盖到最远点的坐标
[代码实现]
#include<bits/stdc++.h>
using namespace std;
#define inl inline
#define mid ((l+r)>>1)
const int N = 2e3 + 5;
inl int read()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m1 , m2 , a[N] , k1[N] , k2[N] , f[N][N];
//k1[i] 表示在第 i 个位置用普通法杖最远可以覆盖到的最远点的下标
//k2[i] 就是用高级法杖
//f[i][j] 表示用 i 个普通法杖和 j 个高级法杖能到的(所有特殊点都覆盖)的最远点
int check ( int x )
{
memset ( k1 , 0 , sizeof k1 );
memset ( k2 , 0 , sizeof k2 );
memset ( f , 0 , sizeof f );
for ( int i = 1 , j ; i <= n ; i ++ )
{
int q1 = a[i] + x - 1 , q2 = a[i] + 2 * x - 1;
//法杖从a[i]出发最远能覆盖的点的位置
j = i;
while ( q1 >= a[j] && j <= n ) j ++; //最远能覆盖的点的下标
k1[i] = j - 1;
j = i;
while ( q2 >= a[j] && j <= n ) j ++;
k2[i] = j - 1;
}
k1[n + 1] = k2[n + 1] = n;
for ( int i = 0 ; i <= m1 ; i ++ )
for ( int j = 0 ; j <= m2 ; j ++ )
{
if ( i > 0 ) f[i][j] = max ( f[i][j] , k1[f[i-1][j]+1] );
if ( j > 0 ) f[i][j] = max ( f[i][j] , k2[f[i][j-1]+1] );
}
return f[m1][m2] == n;
}
signed main ()
{
n = read() , m1 = read() , m2 = read();//一级法杖的使用次数 二级法杖的使用次数
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
if ( m1 + m2 >= n ) { printf ( "1" ); return 0; }
sort ( a + 1 , a + n + 1 );
int l = 0 , r = 1e9;
while ( l <= r )
{
if ( check (mid) ) r = mid - 1;
else l = mid + 1;
}
printf ( "%d" , l );
return 0;
}
G. 4.跳石头
[题目描述]
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有\(N\)块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走M块岩石(不能移走起点和终点的岩石)。
[输入格式]
第一行包含三个整数\(L,n,m\)分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。
接下来\(n\)行,每行一个整数,第\(i\)行的整数\(D_i\),表示第\(i\)块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。
[输出格式]
一个整数,即最短跳跃距离的最大值。
[算法分析]
[代码实现]
#include<bits/stdc++.h>
using namespace std;
#define inl inline
#define mid ((l+r)>>1)
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , L , a[N];
int check ( int x )
{
int ans = 0 , now = 0;
for ( int i = 1 ; i <= n ; i ++ )
{
if ( a[i] - a[now] < x ) ans ++;//去掉
else now = i;//不去掉
}
return ans <= m;
}
signed main ()
{
L = read() , n = read() , m = read();
//起点到终点的距离 起点和终点之间的岩石数量 以及组委会至多移走的岩石数量
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
sort ( a + 1 , a + n + 1 );
a[n+1] = L;
int l = 0 , r = L;//二分最大距离
while ( l <= r )
{
if ( check ( mid ) ) l = mid + 1;
else r = mid - 1;
}
printf ( "%d" , r );
return 0;
}
H. 5.飞离地球
[题目描述]
你现在要从标号为\(1\)的星球到标号为\(n\)的星球。
某一些星球之间有航线,由于超时空隧道的存在,从一个星球到另一个星球时间可能会倒流,而且,从星球\(a\)到\(b\)耗费的时间和星\(b\)到\(a\)耗费的时间不一定相同。
宇宙法规定:" 禁止在出发时间前到达目的地。"
每艘飞船上都有速度调节装置,可以调节飞行的时间。其功能可以使得整次航程中所有两星球间的飞行时间增加或减少相同的整数值。你的任务是帮助它调整速度调节器,找出一条最短时间到达目的地的路径。
[输入格式]
输入文件包含多组数据,第一个数为\(T\),表示数据组数。
对于每组数据,输入第一行为两个正整数\(n,m\),为星球的个数和星球间的路线数。接下来\(m\)行,每行三个整数\(i,j\)和,表示由星球 到星球 飞行的时间为 。由 到 最多只会有一条飞行线路。
[输出格式]
输出文件共\(T\)行,每组数据输出一行。
如果可以通过调节速度调节器完成任务,则输出一个非负整数,表示由星球 到星球 的最短时间。
如果不能由星球 到达星球 ,则输出 -1。
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define mid ((l+r)>>1)
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
return x * f;
}
int n , m , flag , vis[N][N];
int cnt , head[N];
struct node { int to , nxt , w; } e[N*N];
void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
int q[N*N] , dis[N] , in[N] , hh , tt , cntt[N];
int spfa ( int s , int d )
{
for ( int i = 1 ; i <= n ; i ++ ) dis[i] = inf , in[i] = 0 , cntt[i] = 0;
hh = 1 , tt = 0 , dis[s] = 0;
q[++tt] = s , in[s] = 1 , cntt[s] = 1;
while ( hh <= tt )
{
int u = q[hh++]; in[u] = 0;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( dis[v] > dis[u] + e[i].w + d && vis[v][n] )
{
cntt[v] = cntt[u] + 1;
dis[v] = dis[u] + e[i].w + d;
if ( cntt[v] > n ) return 1;
if ( !in[v] ) q[++tt] = v , in[v] = 1;
}
}
}
return dis[n] < 0;//????
}
void init()
{
flag = cnt = 0;
for ( int i = 1 ; i <= n ; i ++ ) head[i] = 0;
for ( int i = 1 ; i <= n ; i ++ ) for ( int j = 1 ; j <= n ; j ++ ) vis[i][j] = 0;
}
int floyd()
{
for ( int i = 1 ; i <= n ; i ++ ) vis[i][i] = 1;
for ( int k = 1 ; k <= n ; k ++ )
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= n ; j ++ )
vis[i][j] = vis[i][j] | ( vis[i][k] & vis[k][j] );
}
signed main ()
{
int T = read();
while ( T -- )
{
n = read() , m = read(); init();
for ( int i = 1 , u , v , w ; i <= m ; i ++ ) u = read() , v = read() , w = read() , add ( u , v , w ) , vis[u][v] = 1;
floyd();
if ( !vis[1][n] ) { printf ( "-1\n" ); continue; }
int l = -1000005 , r = 1000005;
while ( l <= r )//二分一个每条边增加或减少的时间
{
if ( spfa ( 1 , mid ) ) l = mid + 1;
else r = mid - 1;
}
spfa ( 1 , l );//再跑一遍
printf ( "%d\n" , dis[n] );
}
return 0;
}

浙公网安备 33010602011771号