二分答案杂题选做

无特殊注明,所有数据范围都有下界 \(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;
}

原文地址:https://www.cnblogs.com/CDOI-24374/p/15612351.html