• 二分答案杂题选做


    无特殊注明,所有数据范围都有下界 \(0\)(或 \(1\)) .

    代码要不放个 submission id?我看不行。

    1. 聪明的质监员

    题面

    有俩长度为 \(n\) 的序列 \(w_i, v_i\)\(m\) 个区间 \([l_i, r_i]\),给一个整数 \(S\) .

    找到一个 \(k\) 使得

    \[\left|S-\sum_{i=1}^m\left(\left(\sum_{j=l_i}^{r_i}[w_j\ge k]\right)\cdot\left(\sum_{j=l_i}^{r_i}[w_j\ge k]v_j\right)\right)\right| \]

    最小 . 求这个最小值


    \(n,m\le 200000\)\(w_i,v_i\le 10^6\)\(s\le10^12\).

    题解

    二分答案(因为 \(k\) 太大时上面的 \([w_j\ge k] = 0\),所以右界可以设 \(\max w_i\)),结果变成给一个 \(k\),求那个巨大长柿子 .

    发现当 \(k\) 一定的时候,第一个 \(\sum\) 求和出来的每项中,\([w_j\ge k]\)\(v_j\) 都是永恒不变的,只有区间 \([l_i, r_i]\) 变了

    于是前缀和,单次查询就是 \(O(n + m)\) .

    于是总复杂度为 \(O((n+m)\log(\max w_i))\) .

    代码

    #define int long long
    using namespace std;
    typedef long long ll;
    const int N = 200500;
    int n, m, s;
    int w[N], v[N], L[N], R[N];
    ll psumA[N], psumB[N];
    ll calc(int W) // calc s
    {
    	for (int i=1; i<=n; i++)
    	{
    		psumA[i] = psumA[i-1] + (w[i] > W);
    		psumB[i] = psumB[i-1] + (w[i] > W) * v[i];
    	}
    	ll ans = 0;
    	for (int i=1; i<=m; i++)
    		ans += (psumA[R[i]] - psumA[L[i]-1]) * (psumB[R[i]] - psumB[L[i]-1]);
    	return ans;
    }
    signed main()
    {
    	scanf("%lld%lld%lld", &n, &m, &s);
    	int l = 0, r = 0;
    	for (int i=1; i<=n; i++) scanf("%lld%lld", w+i, v+i), r = max(r, w[i]);
    	for (int i=1; i<=m; i++) scanf("%lld%lld", L+i, R+i);
    	ll ans = 0x3f3f3f3f3f3f3f3f; r += 10;
    	while (l <= r)
    	{
    		int mid = (l + r) >> 1;
    		ll x = calc(mid);
    		if (x > s) l = mid + 1;
    		else r = mid - 1;
    		ans = min(ans, abs(x - s));
    	} printf("%lld\n", ans);
    	return 0;
    }
    

    2. Monthly Expense 月度开支

    题面

    给一个长度为 \(n\) 的序列 \(a\),把它分成连续的 \(m\) 段,要求每段数之和的最大值尽可能小,输出这个值 .


    \(n\le 10^5\)

    题解

    二分答案,贪心判定 .

    代码

    typedef long long ll;
    const int N = 200500;
    int n, m, a[N];
    bool check(int t)
    {
    	int cnt = 0, sum = 0;
    	for (int i=1; i<=n; i++)
    	{
    		if (a[i] > t) return false;
    		int already = sum + a[i];
    		if (already > t){++cnt; sum = a[i];}
    		else sum = already;
    	} return cnt + 1 <= m;
    }
    int main()
    {
    	scanf("%d%d", &n, &m);
    	int l = 0, r = 0; // max r = 1e9 < INT_MAX
    	for (int i=1; i<=n; i++) scanf("%d", a+i), r += a[i];
    	int ans = 0x3f3f3f3f;
    	while (l <= r)
    	{
    		int mid = (l + r) >> 1;
    		if (check(mid)){r = mid - 1; ans = min(ans, mid);}
    		else l = mid + 1;
    	}
    	printf("%d\n", ans);
    	return 0;
    }
    

    3. 栅栏

    题面

    有俩长度分别为 \(m,n\) 的序列 \(a,b\),我们有操作

    指定一个 \(1\le k\le m\),先令 \(m\gets m+1\),并令 \(a_m \gets r\)\(a_k = a_k \gets r\),其中 \(r\) 是任意整数

    要求在每个时刻都不能有元素 \(\le 0\) .

    先进行若干次操作,问有多少对 \((i, j)\) 使得 \(a_i = b_j\),注意每个 \(i\) 和每个 \(j\) 都互不相同 .


    \(m\le 50\)\(n\le 1000\),序列的元素不超过 \(32767\) .

    题解

    二分答案,于是问题变为判定取 \(a\) 的前 \(r\) 个元素组成序列 \(a'\) 是否可行.

    这可以 dfs 判断,但是会 TLE,优化方法有二:

    • 随机化
    • 剪枝

    我们自然是不喜欢剪枝的,于是随机化哈哈 .

    具体说,就是随机那个 \(a\) 序列然后上贪心,跑个 3000 次就行了

    这种魔法一般被称作:随机化贪心 .

    剪枝做法比较高妙,可以看洛谷 P1528 / P2329 题解(dX!dX!dX!).

    代码(随机)

    有个魔法:https://www.cnblogs.com/CDOI-24374/articles/15612874.html

    但是没用。

    暴力:

    using namespace std;
    mt19937 rnd(random_device{}());
    typedef long long ll;
    const int M = 51, N = 2222;
    int n, m, a[M], b[N], c[N], sum, now = 0;
    bool check(int t)
    {
    	for (int i=1; i<3333; i++)
    	{
    		memcpy(c, a, sizeof a);
    		shuffle(c+1, c+1+m, rnd); // C++11
    		bool vis = true;
    		for (int i = t; i >= 1; i--)
    		{
    			bool now = false;
    			for(int j = 1; j <= n; j++)
    				if (c[j] >= b[i]){c[j] -= b[i]; now = true; break;}
    			if (!now){vis = false; break;}
    		}
    		if (vis) return true;
    	} return false;
    }
    int main()
    {
    	scanf("%d", &m);
    	for (int i=1; i<=m; i++) scanf("%d", a + i), sum += a[i];
    	scanf("%d", &n);
    	for (int i=1; i<=n; i++) scanf("%d", b + i);
    	sort(b + 1, b + 1 + n);
    	int l = 0, r = n, ans = 0;
    	while (l <= r)
    	{
    		int mid = (l + r) >> 1;
    		if (check(mid)){l = mid + 1; ans = max(ans, mid);}
    		else r = mid - 1;
    	} printf("%d\n", ans); 
    	return 0;
    }
    

    代码(贪心)

    using namespace std;
    typedef long long ll;
    const int M = 51, N = 2222;
    int n, m, a[M], b[N], c[N], ps[N], sum, now = 0;
    bool dfs(int d, int lst) // d 目前数量 
    {
    	if (d<=0) return true;
    	if (now + ps[d] > sum) return false; //
    	for (int i = lst; i<=m; i++)
    	{
    		if (c[i] >= b[d])
    		{
    			c[i] -= b[d];
    			if (a[i] < b[1]) now += c[i];
    			if (dfs(d-1, (b[d] == b[d-1]) ? i : 1)) return true;
    			if (c[i] < b[1]) now -= c[i];
    			c[i] += b[d];
    		}
    	} return false;
    }
    bool check(int t){memcpy(c, a, sizeof a); now = 0; return dfs(t, 1);}
    int main()
    {
    	scanf("%d", &m);
    	for (int i=1; i<=m; i++) scanf("%d", a + i), sum += a[i];
    	scanf("%d", &n);
    	for (int i=1; i<=n; i++) scanf("%d", b + i);
    	sort(a+1, a+1+m); sort(b+1, b+1+n);
    	for (int i=1; i<=n; i++) ps[i] = ps[i-1] + b[i];
    	while (ps[n] > sum) --n;
    	int l = 0, r = n, ans = 0;
    	while (l <= r)
    	{
    		int mid = (l + r) >> 1;
    		if (check(mid)){l = mid + 1; ans = max(ans, mid);}
    		else r = mid - 1;
    	} printf("%d\n", ans); 
    	return 0;
    }
    

    4. 覆盖问题

    题面

    给若干个点,用三个 \(L\times L\) 的正方形覆盖它们,求 \(L\) 的最小值 .


    点数量不超过 \(20000\) .

    题解

    二分答案,就变成给一个 \(L\),问三个 \(L\times L\) 正方形能不能覆盖了 .

    首先肯定有两个放在角上,枚举咋放,问题就变成给若干点,问一个 \(L\times L\) 正方形能不能覆盖力 .

    这个有手就行,于是问题解决 .

    代码

    因为我写的不知道哪里错了,用数据点分治过的,所以不放了 .

    5. 木棍分割

    题面

    \(n\) 根木棍,第 \(i\) 根木棍的长度为 \(l_i\)\(n\) 根木棍依次连结了一起,总共有 \(n-1\) 个连接处. 现在允许你最多砍断 \(m\) 个连接处, 砍完后 \(n\) 根木棍被分成了很多段,要求满足总长度最大的一段长度最小,并且输出有多少种砍的方法使得总长度最大的一段长度最小,并将结果对 \(10007\) 取模 .


    \(n\le 50000\)\(m\le \min\{n - 1, 1000\}\) .

    题解

    二分部分和 \(2\) 一样,方案数就简单 dp 一下 .

    即设 \(dp_{i,j}\) 为到第 \(i\) 根, 分 \(j\) 处,转移随便转 .

    可以前缀和优化,转移点也可以预处理,甚至还可以滚数组哈哈 .

    代码和题解有出入 .

    代码

    using namespace std;
    
    typedef long long ll;
    const int N = 55555, M = 1111, P = 10007, I = 0x3f3f3f3f;
    int n, m, a[N], rt[N], S[N], dp[N];
    void updateS(){for (int i=1; i<=n; i++) S[i] = S[i-1] + dp[i];} // 更新前缀和
    bool check(int k)
    {
        int cnt = 0, sum = 0;
        for (int i=1; i<=n; i++)
        {
            if (sum + a[i] > k){++cnt; sum = a[i];}
            else sum += a[i];
            if (cnt > m) return false;
        } return cnt <= m;
    }
    int main()
    {
        scanf("%d%d", &n, &m);
        int l = 0, r = 0, ans = I;
        for (int i=1; i<=n; i++) scanf("%d", a+i), l = max(l, a[i]), S[i] = S[i-1] + a[i];
        r = S[n];
        while (l < r)
        {
        	int mid = (l + r) >> 1;
        	if (check(mid))r = ans = mid;
            else l = mid + 1; 
        }
        printf("%d ", ans);
        int now = 0;
        for (int i=1; i<=n; i++)
            for (; now<i; now++)
                if (S[i] - S[now] <= ans){rt[i] = now; break;}
        for (int i=1; i<=n; i++) dp[i] = (S[i] <= ans);
        int A = (S[n] <= ans);
        updateS();
        for (int i=2; i<=m+1; i++)
        {
            for (int j=1; j<=n; j++)
            {
                dp[j] = S[j-1];
                if (rt[j]-1 >= 0)
    				dp[j] = (dp[j] - S[rt[j]-1] + P ) % P;
            } updateS(); A = (A + dp[n]) % P; 
        }
        printf("%d\n", A);
        return 0;
    }
    

    6. 猜数游戏

    题面

    有一个任意两数不同且长度为 \(n\) 的序列 \(a\),对它有 \(q\) 个信息,形如 \(a_l, a_{l+1},\cdots,a_r\) 的最小值是 \(r\) .

    问在第几个信息时产生矛盾?


    \(n\le 10^6\)\(q\le 25000\) .

    题解

    二分答案,然后用这个:[link](https://www.cnblogs.com/CDOI-24374/p/15624655.html

    代码

    
    
    // https://darkbzoj.tk/submission/165217
    
    #include <iostream>
    #include <cmath>
    #include <ctime>
    #include <cstdio>
    #include <cstdlib>
    #include <algorithm>
    #include <vector>
    #include <cstring>
    #include <bits/c++0x_warning.h> // *
    
    using namespace std;
    
    const int N = 1e6+500, Q = 25555, I = 0x3f3f3f3f;
    
    inline void chkmin(int& a, int b){if (a > b) a = b;}
    inline void chkmax(int& a, int b){if (a < b) a = b;}
    
    struct Input
    {
    	int l, r, v;
    	bool operator < (const Input& x)const{return v > x.v;}
    }inp[Q], t[Q];
    int n, q;
    
    struct Magic
    {
    	int fa[N]; // dsu
    	void init(){for (int i=0; i<=n+3; i++) fa[i] = i;}
    	void clear(){init();}
    	int get(int x){return fa[x] == x ? x : fa[x] = get(fa[x]);}
    	void merge(int l, int r)
    	{
    		for (int u = l; u <= r; u++)
    			fa[get(u)] = get(r+1); // dsu union
    	}
    	bool crs(int l, int r){return get(l) > r;}
    	Magic(){init();}
    }T;
    
    bool check(int r) // -> is NOT true
    {
    	T.clear();
    	for (int i=1; i<=r; i++) t[i] = inp[i];
    	sort(t+1, t+1+r);
    	int lmin, lmax, rmin, rmax;
    	lmin = lmax = t[1].l; rmin = rmax = t[1].r;
    	for (int i=2; i<=r; i++)
    	{
    		if (t[i].v == t[i-1].v) // Case 1
    		{
    			lmin = min(lmin, t[i].l); lmax = max(lmax, t[i].l);
    			rmin = min(rmin, t[i].r); rmax = max(rmax, t[i].r);
    			if (rmin < lmax) return true;
    			continue;
    		}                       // Case 2
    		if (T.crs(lmax, rmin)) return true;
    		T.merge(lmin, rmax);
    		lmin = lmax = t[i].l; rmin = rmax = t[i].r;
    	}
    	return T.crs(lmax, rmin);
    }
    
    int main()
    {
    	scanf("%d%d", &n, &q); T.init();
    	for (int i=1, l, r, a; i <= q; i++) scanf("%d%d%d", &inp[i].l, &inp[i].r, &inp[i].v);
    	
    	int l = 1, r = q, ans = I;
    	while (l <= r)
    	{
    		int mid = (l + r) >> 1;
    		if (check(mid)){r = mid - 1; ans = min(ans, mid);}
    		else l = mid + 1;
    	}
    	if (ans == I) puts("0");
    	else printf("%d\n", ans);
    	return 0;
    }
    

    7. Exercise 奶牛健美操

    题面

    给一棵 \(n\) 个点的树,割 \(m\) 条边,让直径最小,输出最小直径 .


    \(1\le m < n \le 10^5\) .

    题解

    二分答案,就变成要直径 \(\le M\),那么至少要割多少条边 .

    我们知道直径就是最长链和次长链拼起来,于是可以 dp .

    代码

    using namespace std;
    const int N = 222222;
    vector<int> g[N];
    int n, s, dp[N], t[N], ans = 0;
    void addedge(int u, int v){g[u].push_back(v);}
    void ade(int u, int v){addedge(u, v); addedge(v, u);}
    // 直径:最大链和次大链拼起来
    void dfs(int u, int fa, int p)
    {
    	int l = g[u].size(), cc = 0;
    	for (int i=0; i<l; i++)
    		if (g[u][i] != fa) dfs(g[u][i], u, p);
    	for (int i=0; i<l; i++)
    		if (g[u][i] != fa) t[++cc] = dp[g[u][i]] + 1;
    	sort(t + 1, t + 1 + cc);
    	int ptr = cc;
    	while (ptr && (t[ptr] + t[ptr - 1] > p)){--ptr; ++ans;}
    	dp[u] = t[ptr];
    } 
    bool check(int p)
    {
    	ans = 0; dfs(1, 0, p);
    	return ans <= s;
    }
    int main()
    {
    	scanf("%d%d", &n, &s);
    	for (int i=1, u, v; i<n; i++) scanf("%d%d", &u, &v), ade(u, v);
    	int l = 1, r = n, ans = 0x3f3f3f3f;
    	while (l <= r)
    	{
    		int mid = (l + r) >> 1;
    		if (check(mid)){r = mid - 1; ans = min(ans, mid);}
    		else l = mid + 1;
    	} printf("%d\n", ans);
        return 0;
    }
    
    
  • 相关阅读:
    编程的发展史及Python简介
    计算机的主要部件和组成
    编程二连
    数据库中的数据类型
    数据库概念和基本操作(mysql的操作)
    并发编程之协程
    并发编程之多线程
    网络编程之socket编程
    并发编程之多进程
    网络编程之网络协议
  • 原文地址:https://www.cnblogs.com/CDOI-24374/p/15612351.html
Copyright © 2020-2023  润新知