垃圾ACMer的暑假训练220713
BIT
5.8 Yet Another Problem About Pairs Satisfying an Inequality
题意 ( 2 s 2\ \mathrm{s} 2 s)
原题指路:https://codeforces.com/contest/1703/problem/F
有 t ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t (1≤t≤1000)组测试数据.给定一个元素在 [ 0 , 1 e 9 ] [0,1\mathrm{e}9] [0,1e9]内的、长度为 n ( 2 ≤ n ≤ 2 e 5 ) n\ \ (2\leq n\leq 2\mathrm{e}5) n (2≤n≤2e5)的序列 a [ ] a[] a[].求 ( i , j ) ( 1 ≤ i , j ≤ n ) s . t . a i < i < a j < j (i,j)\ \ (1\leq i,j\leq n)\ s.t.\ a_i<i<a_j<j (i,j) (1≤i,j≤n) s.t. ai<i<aj<j.所有测试数据的 n n n之和不超过 2 e 5 2\mathrm{e}5 2e5.注意答案可能爆int.
思路I
将 a i < i < a j < j a_i<i<a_j<j ai<i<aj<j拆为三部分:① a i < i a_i<i ai<i;② i < a j i<a_j i<aj;③ a j < j a_j<j aj<j.其中①和③扫一遍序列检查即可,则只需统计满足②的数的个数.
可用指针 j j j扫一遍序列,用一个vector存在 j j j之前出现的下标 i i i,则每个 j j j对答案的贡献是vector中满足 i < a j i<a_j i<aj的数的个数,可用二分求得.最后将当前下标 j j j插入vector中即可.
代码I
const int MAXN = 2e5 + 5;
int n;
int a[MAXN];
int main() {
CaseT{
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
ll ans = 0;
vi tmp;
for (int i = 1; i <= n; i++) {
if (a[i] >= i) continue;
ans += (ll)(lower_bound(all(tmp), a[i]) - tmp.begin()); // 求使得i<a_j的个数
tmp.push_back(i);
}
cout << ans << endl;
}
}
思路II
统计满足 i < a j i<a_j i<aj的 i i i的个数可用BIT实现.
代码II
const int MAXN = 2e5 + 5;
int n;
int a[MAXN];
int BIT[MAXN];
ll pre[MAXN]; // 存BIT求得的前缀和
void add(int x, int v) { // a[x]+=v
for (int i = x; i <= n; i += lowbit(i)) BIT[i] += v;
}
ll query(int x) {
ll res = 0;
for (int i = x; i; i -= lowbit(i)) res += BIT[i];
return res;
}
int main() {
CaseT{
cin >> n;
for (int i = 0; i <= n; i++) BIT[i] = 0;
ll ans = 0;
for (int i = 1; i <= n; i++) {
int x; cin >> x;
if (x < i) {
if (x - 1 >= 0) ans += pre[x - 1]; // 满足i<a_j的i的个数是BIT[0...(x-1)]的前缀和
add(x + 1, 1); // x的下标从1开始
}
pre[i] = query(i); // 记录前缀和
}
cout << ans << endl;
}
}
DP
9.1 Good Key, Bad Key ( 3 s ) (3\ \mathrm{s}) (3 s)
题意
原题指路:https://codeforces.com/contest/1703/problem/G
有编号 1 ∼ n 1\sim n 1∼n的 n n n个箱子,其中第 i ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i (1≤i≤n)个箱子有 a i a_i ai个金币.
有两种钥匙可以开箱子:①好钥匙,价值 k k k个金币;②坏钥匙,无需花费,但包括当前准备打开的箱子在内的之后的箱子中的金币数都会变为原来的一半(下取整).严格地说,用坏钥匙打开第 i i i个箱子,会让 a i = ⌊ a i 2 ⌋ , a i + 1 = ⌊ a i + 1 2 ⌋ , ⋯ , a n = ⌊ a n 2 ⌋ a_i=\left\lfloor\dfrac{a_i}{2}\right\rfloor,a_{i+1}=\left\lfloor\dfrac{a_{i+1}}{2}\right\rfloor,\cdots,a_n=\left\lfloor\dfrac{a_n}{2}\right\rfloor ai=⌊2ai⌋,ai+1=⌊2ai+1⌋,⋯,an=⌊2an⌋.每把钥匙只能用一次.
初始时,你没有钥匙和金币.现你要依次打开所有箱子.规定途中你的金币数可以为负.求打开所有箱子后剩余的金币的最大值.
有 t ( 1 ≤ t ≤ 1 e 4 ) t\ \ (1\leq t\leq 1\mathrm{e}4) t (1≤t≤1e4)组测试数据.每组测试数据第一行输入两整数 n , k ( 1 ≤ n ≤ 1 e 5 , 0 ≤ k ≤ 1 e 9 ) n,k\ \ (1\leq n\leq 1\mathrm{e}5,0\leq k\leq 1\mathrm{e}9) n,k (1≤n≤1e5,0≤k≤1e9),分别表示箱子数和一把好钥匙的价格.第二行输入 n n n个数 a i ( 0 ≤ a i ≤ 1 e 9 ) a_i\ \ (0\leq a_i\leq 1\mathrm{e}9) ai (0≤ai≤1e9),分别表示每个箱子的金币数.数据保证所有测试数据的 n n n之和不超过 1 e 5 1\mathrm{e}5 1e5.
对每组测试数据,输出打开所有箱子剩余的金币的最大值,注意答案可能爆int.
思路I
显然开到第 i i i个箱子时使用的坏钥匙不超过 min ( i , 31 ) \min(i,31) min(i,31)把. d p [ i ] [ j ] dp[i][j] dp[i][j]表示开到第 i i i个箱子、用了 j j j把坏钥匙的后剩余金币的最大值,则 a n s = max 1 ≤ j ≤ 31 d p [ n ] [ j ] ans=\displaystyle\max_{1\leq j\leq 31}dp[n][j] ans=1≤j≤31maxdp[n][j].
因用坏钥匙会将后面的箱子的收益减半,则可在输入 a [ i ] a[i] a[i]时预处理出 a [ i ] [ ] a[i][] a[i][],其中 a [ i ] [ j ] = a [ i ] [ 0 ] > > j a[i][j]=a[i][0]>>j a[i][j]=a[i][0]>>j.
枚举当前所用的坏钥匙数 j ( 0 ≤ j ≤ min { i , 31 } ) j\ \ (0\leq j\leq \min\{i,31\}) j (0≤j≤min{i,31}).对每个 j j j,按第 i i i个箱子用好钥匙还是坏钥匙分类:
①用好钥匙: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] − k + a [ i ] [ j ] dp[i][j]=dp[i-1][j]-k+a[i][j] dp[i][j]=dp[i−1][j]−k+a[i][j].
②用坏钥匙 ( j > 0 ) (j>0) (j>0): d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] , 1 ≤ j < 31 d p [ i − 1 ] [ j ] , j = 31 dp[i][j]=\begin{cases}dp[i-1][j-1]+a[i][j],1\leq j<31 \\ dp[i-1][j],j=31\end{cases} dp[i][j]={dp[i−1][j−1]+a[i][j],1≤j<31dp[i−1][j],j=31.
最终 d p [ i ] [ j ] dp[i][j] dp[i][j]为三者取 max \max max.
代码I
const int MAXN = 1e5 + 5;
int n, k; // 箱子数、钥匙数
int a[MAXN][32]; // a[i][j]=a[i][0]>>j
ll dp[MAXN][32]; // dp[i][j]表示开到第i个箱子、用j把坏钥匙的最大收益
int main() {
CaseT{
// memset(dp, 0, so(dp)); // 会TLE
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i][0];
dp[i][0] = 0;
for (int j = 0; j <= min(i, 31); j++) {
dp[i][j] = 0;
a[i][j] = a[i][0] >> j; // 预处理a[i][]
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= min(i, 31); j++) { // 最多用i把钥匙
dp[i][j] = dp[i - 1][j] - k + a[i][j]; // 当前用好钥匙
if (j) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + a[i][j]); // 当前用坏钥匙
if (j == 31) dp[i][j] = max(dp[i][j], dp[i - 1][j]); // j=31时已无收益
}
}
ll ans = -INFF;
for (int j = 0; j <= 31; j++) ans = max(ans, dp[n][j]);
cout << ans << endl;
}
}
代码II:记搜
int main() {
CaseT{
int n,k; cin >> n >> k;
vl a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
vector<vl> dp(n + 1, vl(32, -INFF)); // dp[i][j]表示开到第i个箱子、用j把坏钥匙的最大收益
function<ll(int, int)> solve = [&](int i, int j)->ll { // 记搜求dp[i][j]
if (i > n || j == 31) return 0;
if (dp[i][j] != -INFF) return dp[i][j];
return dp[i][j] = max(solve(i + 1, j) - k + (a[i] >> j), solve(i + 1, j + 1) + (a[i] >> (j + 1)));
};
cout << solve(1, 0) << endl; // 当前开到第1个箱子,用了0把坏钥匙
}
}
思路II
最优策略是先对前缀用好钥匙,剩下的用坏钥匙.
[证] 考察相邻两个箱子 a i a_i ai和 a i + 1 a_{i+1} ai+1.
①若先用坏钥匙,再用好钥匙,则贡献为 ⌊ a i 2 ⌋ + ⌊ a i + 1 2 ⌋ − k \left\lfloor\dfrac{a_{i}}{2}\right\rfloor+\left\lfloor\dfrac{a_{i+1}}{2}\right\rfloor-k ⌊2ai⌋+⌊2ai+1⌋−k.
②若先用好钥匙,再用坏钥匙,则贡献为 a i + ⌊ a i + 1 2 ⌋ − k a_{i}+\left\lfloor\dfrac{a_{i+1}}{2}\right\rfloor-k ai+⌊2ai+1⌋−k.显然后者大于前者.
代码III
int main() {
CaseT{
int n,k; cin >> n >> k;
vl a(n + 1), pre(n + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}
ll ans = -INFF;
for (int i = 0; i <= n; i++) { // 枚举用好钥匙的前缀,i=0时表示全用坏钥匙
ll res = pre[i] - (ll)k * i; // 用了i把坏钥匙
for (int j = i + 1; j <= min(i + 31, n); j++) // 枚举用了几把坏钥匙
res += a[j] >> (j - i);
ans = max(ans, res);
}
cout << ans << endl;
}
}
二分
2.6 Educational Codeforces Round 131 (Rated for Div. 2) C. Schedule Management
题意 ( 2 s 2\ \mathrm{s} 2 s)
原题指路:https://codeforces.com/contest/1701/problem/C
有编号 1 ∼ n 1\sim n 1∼n的 n n n个工人和编号 1 ∼ m 1\sim m 1∼m的 m m m项工作,每项工作有一个值 a i a_i ai,表示擅长这项工作擅长的工人的编号.若工人做一项其擅长的工作,则需花费 1 h 1\ \mathrm{h} 1 h,否则需花费 2 h 2\ \mathrm{h} 2 h.每个工人同时只能做一项工作.
有 t ( 1 ≤ t ≤ 1 e 4 ) t\ \ (1\leq t\leq 1\mathrm{e}4) t (1≤t≤1e4)组测试数据.每组测试数据第一行输入两整数 n , m ( 1 ≤ n ≤ m ≤ 2 e 5 ) n,m\ \ (1\leq n\leq m\leq 2\mathrm{e}5) n,m (1≤n≤m≤2e5),分别表示工人数、工作数.第二行输入 m m m个整数 a i ( 1 ≤ a i ≤ n ) a_i\ \ (1\leq a_i\leq n) ai (1≤ai≤n),分别表示第 i i i项工作擅长的工人的编号.数据保证所有测试数据的 m m m之和不超过 2 e 5 2\mathrm{e}5 2e5.
对每组测试数据,输出完成所有工作的最少时间.
思路
用 c n t [ i ] cnt[i] cnt[i]记录 i i i号工人擅长的工作数.
二分工作时间,check当前时间内能否做完所有工作.显然最长时间不超过 2 m 2m 2m.
显然应让工人在给定时间内先做其擅长的工作,若还有剩余时间,再做其不擅长的工作.设给定时间 t t t内能完成的净工作数为 t a s k task task,则 t t t内能完成所有工作等价于 t a s k ≥ 0 task\geq 0 task≥0.对每个工人 i i i,若 t ≥ c n t [ i ] t\geq cnt[i] t≥cnt[i],则他不仅能做完擅长的工作,还能多做 ⌊ t − c n t [ i ] 2 ⌋ \left\lfloor\dfrac{t-cnt[i]}{2}\right\rfloor ⌊2t−cnt[i]⌋项工作;若 t < c n t [ i ] t<cnt[i] t<cnt[i],则他只能做完 ( c n t [ i ] − t ) (cnt[i]-t) (cnt[i]−t)项工作.
代码
const int MAXN = 2e5 + 5;
int n, m;
int pro[MAXN]; // pro[u]记录事件u擅长的人
int cnt[MAXN]; // cnt[i]表示第i人擅长的事情的数量
bool check(int t) { // 时间t内能否做完
ll task = 0;
for (int i = 1; i <= n; i++) {
if (t >= cnt[i]) task += (ll)(t - cnt[i]) / 2; // 1小时的都能做完,多做一些2小时的
else task -= (ll)(cnt[i] - t);
}
return task >= 0;
}
int main() {
CaseT{
cin >> n >> m;
for (int i = 1; i <= n; i++) cnt[i] = 0;
for (int i = 1; i <= m; i++) {
cin >> pro[i];
cnt[pro[i]]++;
}
int l = 0, r = 2 * m;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << l << endl;
}
}