dodola · blog

💭Codeforces Round 980 (Div. 2)

1,416 words5 min read#算法#竞赛编程#Codeforces#补题记录算法Codeforces 补题记录 9/10

A. Profitable Interest Rate

题意

有两种储值方式——无利可图和盈利,“盈利”可以保证盈利,但是有最低储值要求,“无利可图”类型没有利息,但是可以让“盈利”的最低储值降低。在“无利可图”储值xx元,可以让“盈利”的最低值要求降低2×x2\times x元,最低储值不能低于00元,两种储值均不能取出。现在Alice拥有aa元,并想使存入“盈利”的金额越多越好,求Alice最多存入多少“盈利”类型的金额。

数据范围

  • 1t1041\leq t\leq 10^4
  • 1a,b1091\leq a,b\leq 10^9

思路

假设存入xx元在“无利可图”,则“盈利”的最低储值降为b2×xb-2\times x,此时如果Alice的剩余金额axa-x可以达到最低储值,则答案为axa-x,否则为00,最小化xx即可。

参考代码

void solve() {
ll a, b;
cin >> a >> b;
ll x = max(0ll, b - a);
ll ans = max(0ll, a - x);
cout << ans << '\n';
}

B. Buying Lemonade

题意

柠檬水售卖机有nn个按钮,但是无法辨认每个按钮对应的槽位还剩多少柠檬水,购买者知道每个槽位最初的柠檬水数量,每按下一个按钮,如果该按钮对应的槽位还有柠檬水,则可以售出11瓶柠檬水,若槽位为空,则将没有任何东西掉出来。现在需要精确购买kk瓶柠檬水,保证柠檬水售卖机里有足够的柠檬水,即i=1naik\sum_{i=1}^{n} a_i \ge k。求问最少的可以保证达成任务的点击次数。

数据范围

  • 1t1041\leq t\leq 10^4
  • 1n2×1051\leq n\leq 2\times 10^5
  • 1k1091\leq k\leq 10^9
  • 1ai1091\leq a_i\leq 10^9

nn之和不超过2×1052\times 10^5

思路

基本的贪心购买思路是一直按同一个按钮,直到该槽位为空,考虑到购买kk瓶至少需要kk次基本点击,我们需要最小化会浪费的点击次数,即点空槽位的次数。

假设当前每个槽位都至少有22瓶柠檬水,我们想最小化点空的次数,最好先把每个按钮都点击22次,如果此时的总数足够kk,我们就计数次数,否则去掉空槽位(通过 1 次点击去除),我们要继续最小化。

二分在每个按钮点击的次数,查询能买到的柠檬水总数,计数最少的次数,即第一个满足总数大于等于kk的那个次数。

参考代码

ll a[maxn];
void solve() {
ll n, k;
cin >> n >> k;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a + 1, a + 1 + n);
auto get_cnt = [&](ll cnt) {
ll ret = 0ll, tot = 0ll;
for (ll i = 1; i <= n; i++) {
if (a[i] < cnt) {
if (tot + a[i] < k) {
ret += a[i] + 1;
tot += a[i];
} else {
ret += k - tot;
break;
}
} else {
if (tot + cnt < k) {
ret += cnt;
tot += cnt;
} else {
ret += k - tot;
break;
}
}
}
return ret;
};
auto get_lemonade = [&](ll cnt) {
ll tot = 0ll;
for (ll i = 1; i <= n; i++) {
if (a[i] < cnt) {
tot += a[i];
} else {
tot += cnt;
}
}
return tot;
};
// 可以获得k的第一个次数的下标
ll l = 1, r = n;
while (l < r) {
ll mid = l + (r - l) / 2;
if (get_lemonade(a[mid]) < k) {
l = mid + 1;
} else {
r = mid;
}
}
ll ans = get_cnt(a[l]);
cout << ans << '\n';
}

C. Concatenation of Arrays

题意

nn个二元组,将这些二元组重新排序后让序列的逆序数最小,输出排序后的结果。

数据范围

  • 1t1041\leq t\leq 10^4
  • 1n1051\leq n\leq 10^5
  • 1ai,j1091\leq a_{i,j}\leq 10^9

思路

排序,将二元组按照两数之和、第一位数、第二位数的优先级顺序从小到大排序。

参考代码

void solve() {
ll n;
cin >> n;
vector<pll> v;
for (ll i = 1; i <= n; i++) {
pll p;
cin >> p.first >> p.second;
v.push_back(p);
}
sort(v.begin(), v.end(), [&](pll p1, pll p2) {
return make_tuple(p1.first + p1.second, p1.first, p1.second) <
make_tuple(p2.first + p2.second, p2.first, p2.second);
});
for (auto i : v) {
cout << i.first << ' ' << i.second << ' ';
}
cout << '\n';
}

D. Skipping

题意

问答系统,对第ii个题,如果选择回答,可以获得aia_i分数,接下来只能选择序号j<ij\lt i且没有操作过的问题。也可以选择跳过,接下来可以选择jbij\leq b_i的未操作过的题目。从11号问题开始回答。

思路

注意到,当跳到索引ii时,所有1ki1\leq k\leq i的没有回答过的题目,都可以通过依次回答。我们只需要贪心的选择前置和-到达某个位置的最小代价即可。

考虑用一个全局multisetmultiset维护当前可以使用的代价,从11号问题开始,每个问题的bib_i指向一个到bi+1b_i+1会“失效”的最小代价,每次移动时去除失效代价,并加入新的最小代价,维护最大得分即可。

维护时可以注意到biib_i\leq i的跳题没有意义,不如直接向前答题,可以通过判断去除这种移动。

参考代码

void solve() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}
for (ll i = 1; i <= n; i++) {
cin >> b[i];
}
// ll ans = a[1];
multiset<ll> st; // 可行代价
map<ll, vector<ll>> mp; // 存储到达i的所有可行代价
mp[2].push_back(0);
st.insert(0);
ll ans = a[1];
for (ll i = 1; i <= n; i++) {
for (auto x : mp[i]) {
st.erase(st.find(x)); // 删去不再适用的代价
}
if (st.empty()) {
dis[i] = -1; // 没有可用的代价
} else {
dis[i] = *st.begin();
}
if (dis[i] == -1)
continue;
ans = max(ans, pre[i] - dis[i]);
if (b[i] <= i)
continue;
// 可以拓展到点b[i],b[i]+1时刻这些代价都不再适用
ll d = dis[i] + a[i];
mp[b[i] + 1].push_back(d);
st.insert(d);
}
cout << ans << '\n';
}