洛谷题单【算法1-6 二分查找与二分答案】部分题解&个人向总结

文章讲述了如何将复杂的计算过程通过二分法简化,涉及一元三次方程求解、高考志愿中的最邻近元素查找、路标设置问题、以及银行存款中边还边计算利息的场景,展示了实数二分在优化计算效率中的应用。
摘要由CSDN通过智能技术生成

二分

  • 关键思路:

复杂的分段补充过程 转化为 简单的离散总量单次比较过程 

复杂的模拟check过程( O(n^2,3 * logn) ) 转化为 简单的线性遍历取分量检验过程(O(nlogn))

  • 依题意选择:

check() k lowerbound (循环结束时得到 第一个等于大于目标值的元素 (最小合法解?)

check() > k upperbound (循环结束时得到 第一个大于 目标值的元素) (最大合法解?)

  • 右边界尽量大!!> 1e10!!
//整数
while(left <= right)//二分答案
  {
      int mid = (left + right) >> 1;
      if( check(mid) )
          ans = mid, right = mid - 1; //注意ans的更新;right 和 left上下位置视check()而定
      else
          left = mid + 1;
  }
//实数
double eps = 1e-x;
while( right - left >= eps )
  {
      int mid = (left + right)/2;
      if( check(mid) )
          ans = mid, right = mid - eps;
      else
          left = mid + eps;
  }
  • 一元三次方程实数二分模板 —— l, r赋值偏移步长可设置为小数,其精度越高(越小),结果精度越高,如 r = m - 0.001(偏移步长1e-3) 其结果精度就精确至r = m - 0.01(偏移步长1e-2)小数点多一位,这个偏移步长 可统一定义为eps )

    做题思路:根据一元三次方程图像特性,将其分为三段[l, x1],[x1, x2],[x2, r]后,分别进行二分无限接近 零点( ≤ eps = 1e-6 即视作为0 ) 的求解,之后再保留两位小数(极限零点的精度足够AC了)

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define INF 0x3f       //不能直接作等于赋值
#define MIN 0xc0
typedef unsigned long long ull;

double esp = 1e-6;
int start;
double a, b, c, d;

double f(double x) {
    return a*pow(x, 3) + b*pow(x, 2) + c * x + d;
}

bool check(double x) {
    if( start > 0 ) {
        if( f(x) >= esp ) return 0;
        else  return 1;
    } else {
        if( f(x) >= esp ) return 1;
        else  return 0;

    }
}

void solve() {
    cin >> a >> b >> c >> d;
    double x1 = (-b-sqrt(1.0*b*b-3*a*c)) / (3*a), x2 = (-b+sqrt(1.0*b*b-3*a*c)) / (3*a);
    for( int i = 0; i < 3; i++ ) {
        double l, r, ans;
        switch ( i ) {
        case 0:
            start = ( f(-100) > 0 ) ? 1 : -1; //起始符号情况
            l = -100, r = x1;
            break;
        case 1:
            start = ( f(x1) > 0 ) ? 1 : -1;
            l = x1, r = x2;
            break;
        case 2:
            start = ( f(x2) > 0 ) ? 1 : -1;
            l = x2, r = 100;
            break;
        }
        while( l <= r ) {
            double m = (l + r)/2;
            if( check( m ) ) {
                ans = m; r = m - 0.001; //实数微调原精确,精度越高, 此1e-3即可AC
            } else {
                l = m + 0.001;
            }
        }
        if( i )
            cout << ' ';
        cout << fixed << setprecision(2) << ans;
    }


    return ;
}

signed main()
{
    ios::sync_with_stdio(false); cin.tie(nullptr);      //最好不与scanf和printf混用 除非题目简单
    int t = 1;
//    cin >> t;
    while( t-- ){
        solve();
    }

    return 0;
}
  • 烦恼的高考志愿 (逆向思维,在升序序列中查询最邻近x的元素即 要么是最后一个小于等于x的元素 要么是第一个大于等于x的元素,后者直接在其升序中lowerbound即可,而前者则逆向思考即在降序序列中lowerbound即可,原升序中最后一个小于等于x的元素在降序中则转变为第一个小于等于x的元素了!)

  • *路标设置 —— 最小化_最大切割区间长度(起初陷入了求解如何具体放置路障的难思考误区,实际上仍然可以直接对目标求解得“最小空旷指数”二分验证查找,将放置路标的操作视作一种检查机制,转换思路为验证 最终要达到这个最小空旷值 至少需要多少路障cnt(超过k就需要增大目标值,少于k才可继续减小目标值)的压缩问题,期间cnt += (mxl÷mid),可见mid越小 则需要的cnt越多,而我们要找的就是这个最小mid,且能使得cnt ≤ k,将具体放置分割操作直接简单化为等分验证

    !注意 !(画图推导时发现的这个错点):刚好整除对等分时,所需的路障数为 商-1!如60/20 = 3,实际只需两个等分点!

    图示:

    #include <bits/stdc++.h>
    using namespace std;
    #define int long long
    #define endl '\n'
    #define INF 0x3f   	//不能直接作等于赋值
    #define MIN 0xc0
    typedef unsigned long long ull;
    
    int L, n, k;
    priority_queue<int> pq;
    bool check( int stdL ) {
    	priority_queue<int> tpq = pq; //堆优化优先挑长路段检查
    	int cnt = 0;
    	while( tpq.size() ){
    		int mxl = tpq.top(); tpq.pop();
    		if( mxl > stdL ) {
    			cnt += mxl/stdL;	//直接除得所需切割路标点(无须再放回子路段,默认全部≤stdL)
    			
    			if( mxl % stdL == 0 ) //若是整除等分关系,路标可少一(易漏!
    				cnt--;
    			
    		}
    		if( cnt > k )
    			return 0;	//路标不够,需要增大标定最小值
    	}
    	return 1;			//路标足够,可以继续减小标定最小值
    };
    /*
      115 4 2
      0 20 80 115
      
      30
     */
    void solve() {
    	cin >> L >> n >> k;
    	vector<int> v(n);
    	for( auto &x : v )
    		cin >> x;
    	for( int i = n-1; i > 0; i-- ) {
    		pq.push( v[i] - v[i-1] ); //存放已分割路段
    	}
    	
    	int l = 1, r = L, ans = -1; //用在r初始化上
    	while( l <= r ){
    		int m = (l + r) >> 1;
    	
    		if( check(m) ) {	//减小标定最小值
    			ans = m; r = m - 1;
    		} else {
    			l = m + 1;      //增大标定最小值
    		}
    	}
    	cout << ans;
    	return ;
    }
    
    signed main()
    {
    	ios::sync_with_stdio(false); cin.tie(nullptr);  	//最好不与scanf和printf混用 除非题目简单
    	int t = 1;
    //	cin >> t;
    	while( t-- ){
    		solve();
    	}
    	
    	return 0;
    }
  • *数列分段 —— 最小化_最大区间和(同上思路,不将“如何用现有段数切割区间”作为问题研究重点,而是避重就轻将其作为“检查验证区间和是否达到目标值”的形式存在,在尽量用少的段数,最大化目标值)

    用前缀和优化问题,顺序遍历右端下标,通过s[r] - s[l] 得 s[l+1~r]的区间和并验证其能否 ≥ std(标定要达到的最小区间和),记录分段数cnt,最后必须有 cnt ≤ M则满足check()(不用担心cnt < M是因为前面的区间和都满足大于等于std的要求下,可以直接把靠后剩余的元素全部合并到最后一个区间

    时间复杂度正好是O(nlogn)

    #include <bits/stdc++.h>
    using namespace std;
    #define int long long
    #define endl '\n'
    #define INF 0x3f   	//不能直接作等于赋值
    #define MIN 0xc0
    typedef unsigned long long ull;
    const int N = 1e6;
    
    int n, m;
    int S[N]; //存储前缀和
    
    bool check( int stdS ) {
    	int cnt = 0;
    	for( int l = 0, r = 1; r <= n; r++ ) {
    		if( S[r] - S[l] > stdS ) {
    			l = --r;
    			cnt++;
    		}
    		if( r == n ) cnt ++; //最后划定的一组不要忘了加上!
    		
    		if( cnt > m ) return 0;
    	}
    	return 1;
    };
    
    void solve() {
    	cin >> n >> m;
    	for( int i = 1; i <= n; i++ ) {
    		int x; cin >> x;
    		S[i] = S[i-1] + x;
    	}
    	
    	int l = 1, r = 1e9, ans = -1; 
    	while( l <= r ){
    		int m = (l + r) >> 1;
    		
    		if( check(m) ) {	// 实际分组数 ≤ 标定组数,说明标定值囊括程度大,可以减小这个标定最小值
    			ans = m; r = m - 1;
    		} else {
    			l = m + 1;      //增大标定最小值
    		}
    	}
    	cout << ans;
    	return ;
    }
    
    signed main()
    {
    	ios::sync_with_stdio(false); cin.tie(nullptr);  	//最好不与scanf和printf混用 除非题目简单
    	int t = 1;
    //	cin >> t;
    	while( t-- ){
    		solve();
    	}
    	
    	return 0;
    }
  • 银行存款 (实数二分(注意二分出口r-l≥1e-3设置精度)。注意他是边还边算利息的!即欠的本金是逐步减少的!)

    坑点:初始ans设置为0!!!

    二级结论

  • kotori的设备 (实数二分——check()函数参数为maxT !!表示每台设备在充电宝的延缓下要能达到maxT下的发电量!,即 将规定时间内的发电量作为检查依据,从而 可将复杂的对不同设备不同时间的充电过程视为只给定一定补充量的离散过程,将 每台设备无法达到maxT发电时间的所缺能量总和 与 maxTb总补充量 比较,作为maxT可否通过的判断依据 return (bool)补充量 ≥ 缺乏能量

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define INF 0x3f       //不能直接作等于赋值
#define MIN 0xc0
#define a first
#define b second
typedef unsigned long long ull;
const int N = 2e5;
typedef pair<int, int> PII;

int n, p;
double eps = 1e-6;
PII dv[N];
bool check( double mxt ) {
    double lck = 0, sup = mxt*p;  //缺少量 和 总补充量
    for( int i = 1; i <= n; i++ ){         //O(n)遍历
        double str = 1.0*dv[i].b, nd = 1.0*dv[i].a * mxt;
        lck += (str >= nd) ? 0 : nd - str; //判断储备量是否大于等于所需量
    }
    return sup - lck >= eps; //差距大于等于精确值,视为不少于
}

void solve() {
    cin >> n >> p;
    int sc = 0;
    for( int i = 1; i <= n; i++ ){
        cin >> dv[i].a >> dv[i].b;
        sc += dv[i].a;
    }
    double ans = -1;
    if( p >= sc ) {
        cout << ans;
        return;
    } else {
        double l = 0, r = 1e10; //边界陷阱!尽量往大了取
        while( r - l >= eps ) {
            double m = (r + l)/2;
            if( check(m) ) {
                ans = m; l = m + eps;
            } else {
                r = m - eps;
            }
        }
    }
    cout << fixed << setprecision(10) << ans;
    return ;
}

signed main()
{
    ios::sync_with_stdio(false); cin.tie(nullptr);      //最好不与scanf和printf混用 除非题目简单
    int t = 1;
//    cin >> t;
    while( t-- ){
        solve();
    }

    return 0;
}
  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值