二分答案杂题选做
无特殊注明,所有数据范围都有下界 \(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;
}
以下是博客签名,正文无关
本文来自博客园,作者:Jijidawang,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/15612351.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ