Codeforces Round 922 (Div. 2) 题解(CDE)
Solution of C
做法:贪心分讨首先,看到二进制,并且要求计算一个差值的绝对值,可以考虑拆位,一位一位算。
有一个很常用的结论,就是 \(2^1+2^2+2^3+\dots+2^{x-1} < 2^x\)。于是将题目中的 \(a\) 和 \(b\) 拆位以后,从高位往低位枚举,假如在某一位上产生了差值,后面的每一位无论如何减小这个差值,都不会改变差值的正负。
例:
1001101
1010100
发现从左往右(从高往低)第三位,无论给 \(x\) 的这一位赋什么值,都会使差值改变,这时如果差值是正数,那么后面的数无论让差值如何减小,差值都是正数,反之亦然。
由此,我们得出策略:如果最高位产生的差值是正数,那么后面每一位都尽量让 \(a\) 小于 \(b\),也就是让差值减小。需要注意的是,时刻在意 \(r\) 的限制,由于上面的结论,所以可以贪心的从高往低,能让差值(赋值后满足 \(r\) 的限制)减小就减小,否则就被迫让差值变大。
如果最高位产生的差值是负数,与上面正好相反即可。可以记录两个 \(ans\),一个指最高位产生正数时的答案,一个指最高位产生负数时的答案,两者比较即可。同时会发现,有可能因为 \(r\) 的限制,最高位不能产生正数(或负数),这种情况需要特判,但是易得无论如何的 \(a\) 和 \(b\),至少可以占有一种情况。
Code of C
#include
#define int long long
using namespace std;
auto mread = [](){
int x;
scanf("%lld", &x);
return x;
};
int t = mread();
signed main(){
while(t --){
int a = mread(), b = mread(), r = mread(), ans1 = LONG_LONG_MAX, ans2 = LONG_LONG_MAX;
int tmp = 0, now = 0, ans = LONG_LONG_MAX;
for(int i = 62; i >= 0; i --){
if(((a >> i) & 1) == ((b >> i) & 1))
continue;
else if(tmp == 0){
tmp = 1;
if((a >> i) & 1)
ans1 = (1ll << i);
else{
if((1ll << i) <= r)
now += (1ll << i), ans1 = (1ll << i);
else{
tmp = -1;
break;
}
}
}
else{
if((a >> i) & 1){
if(now + (1ll << i) <= r)
now += (1ll << i), ans1 -= (1ll << i);
else
ans1 += (1ll << i);
}
else
ans1 -= (1ll << i);
}
}
if(tmp == 0){
printf("0\n");
continue;
}
if(tmp != -1)
ans = min(ans, ans1);
tmp = 0, now = 0;
for(int i = 62; i >= 0; i --){
if(((a >> i) & 1) == ((b >> i) & 1))
continue;
else if(tmp == 0){
tmp = 1;
if((a >> i) & 1){
if((1ll << i) <= r)
now += (1ll << i), ans2 = -(1ll << i);
else{
tmp = -1;
break;
}
}
else
ans2 = -(1ll << i);
}
else{
if((a >> i) & 1)
ans2 += (1ll << i);
else{
if(now + (1ll << i) <= r)
now += (1ll << i), ans2 += (1ll << i);
else
ans2 -= (1ll << i);
}
}
}
if(tmp != -1)
ans = min(ans, -ans2);
printf("%lld\n", ans);
}
return 0;
}
Solution of D
做法:二分 + 双指针优化 DP
首先看到问题直接计算不好计算,因为最大值涉及到封锁元素和以及每个片段的元素和,影响因素较多,这时可以考虑直接 DP,但是问题在于 DP 状态不仅要记录下来当前枚举到的元素,还有当前封锁元素和等等,无法直接 DP。此时注意到答案具有单调性,考虑二分,并用 DP 计算 check 函数。
设 \(k\) 为当前 check 函数接收的待验证答案。\(f_{i,1/0}\) 表示当前枚举到第 \(i\) 个元素,封锁(或不封锁)第 \(i\) 个元素时,满足没有任何一个片段的元素和大于 \(k\),此时封锁元素和的最小值。如果最后 \(f_{n,0}\) 和 \(f_{n,1}\) 的较小值小于等于 \(k\),则 check 函数返回 true,否则返回 false。
考虑如何计算 \(f\)。首先是 \(f_{i,1}\) 的情况,很好计算,因为已经封锁了当前元素,前面的元素无论如何修改,只要在不加入当前元素时合法,就永远合法,所以
\(f_{i,1}=\min(f_{i-1,1},f_{i-1,0})+a_i\)
而对于 \(f_{i,0}\) 的情况,我们就要考虑对于一段区间 \((j,i)\),这段区间都不封锁,那么就要满足这段区间的元素和小于等于 \(k\),此时 \(f_{i,0}\) 就可以更新为 \(f_{j-1,1}\)。瓶颈在于找到所有的区间 \((j,i)\) 满足条件,同时找到最小的 \(f_{j-1,1}\) 以更新。
这一步可以利用双指针完成,一个指针就是当前的 \(i\),另一个指针 \(j\),表示区间 \((j,i)\) 的元素和小于等于 \(k\),当 \(i\) 向后推移时,更新 \(j\) 直到不用更新。同时可以把所有的 \(f_{j-1,1}\) 存进一个支持删除的有序集合(比如 multiset)中,更新 \(i\) 和 \(j\) 的时候向集合中插入或删除,每次赋值 \(f_{i,0}\) 的时候取出集合中的最小值即可。
注:作者个人习惯问题,代码中变量名与上述不尽相同,推荐自己写代码,不看参考代码。
Code of D
#include
#define int long long
using namespace std;
auto mread = [](){
int x;
scanf("%lld", &x);
return x;
};
const int MA = 1e14;
int t = mread();
signed main(){
while(t --){
int n = mread();
int a[n + 5], l = 0, r = 0, f[n + 5][2] = {0};
f[0][0] = f[0][1] = 0;
for(int i = 1; i <= n; i ++){
a[i] = mread();
l = max(l, a[i]);
r += a[i];
}
multiset s;
auto check = [&](int x){
s.clear();
for(int i = 1; i <= n; i ++)
f[i][0] = f[i][1] = MA;
int k = 1, now = 0;
s.insert(f[0][1]);
for(int i = 1; i <= n; i ++){
f[i][1] = min(f[i - 1][0], f[i - 1][1]) + a[i];
now += a[i];
while(k < i && now > x){
now -= a[k];
s.erase(s.find(f[k - 1][1]));
k ++;
}
f[i][0] = (*s.begin());
s.insert(f[i][1]);
}
return min(f[n][0], f[n][1]) <= x;
};
while(l < r){
int mid = (l + r) >> 1;
if(check(mid))
r = mid;
else
l = mid + 1;
}
printf("%lld\n", l);
}
return 0;
}
Solution of E
做法:基于整体二分的排序
\(x\) 的值配合上评测机的回答可以提供很多信息,所以我们考虑将这个值尽量掌控在自己手里。
首先,我们可以找到最小值和最大值,这样子就可以自己操作 \(x\) 的值。
假设我们要找到最大值。我们可以从 \(1\) 到 \(n\) 依次询问,如果回答大于号,就继续询问,如果是小于号,就及时止损,询问下一个数,如果是等于号,就可以考虑更新答案。但是到底要不要更新答案呢?我们注意到,虽然我们不能知道 \(x\) 的值,但是可以知道他的变化量,也便是说,可以记录一个 \(now\) 表示变化量,如果收到了大于号,就增加,否则就减小,等到收到了等于号,就判断这个变化量是个正数还是负数,如果是正数,那么当前询问的数就有可能是最大值,否则就不可能。如果是正数,不仅要更新答案,还要更新 \(now\) 为 \(0\),因为我们本质是要比较当前这个等于号对应的数字是否比上一个答案大。
找最小值则反过来即可。
现在我们拿到了最大值和最小值,我们就拿到了随意操控 \(x\) 的权力。由于答案序列是一个排列,所以我们可以考虑排序以获得答案。根据题意易得,我们可以做到将一个数与 \(x\) 值作比较,并且这个 \(x\) 值可以由我们操控,所以可以整体二分,确定 \(x\) 值后将比 \(x\) 小的放在左面,大的放在右面,然后递归处理。
这样共会进行 \(n\log{n}\) 次比较,每次比较需要一次操作比较一次操作还原 \(x\)(比较后 \(x\) 的值会改变),而按照先左再右的递归,\(x\) 的值最多改变 \(n\log{n}\) 次,再加上刚开始寻找最小值和最大值,均是最坏均摊 \(2n\) 次,可以顺利在限定次数内完成任务。
Code of E
#include
#define int long long
using namespace std;
const int N = 2005;
int t, n, now, a1[N], a2[N], ma, mi, a[N], sum;
char c;
void print(int x){
cout << "? " << x << "\n";
cout.flush();
assert(sum <= 40 * n);
return;
}
int makeMin(){
int ans = 0, now = 0, la = 1;
for(int i = 1; i <= n; i ++){
while(1){
print(i);
cin >> c;
if(c == '<')
now --;
else if(c == '>'){
now ++;
break;
}
else if(c == '='){
if(now < la)
ans = i, la = now;
break;
}
}
}
return ans;
}
int makeMax(){
int ans = 0, now = 0, la = -1;
for(int i = 1; i <= n; i ++){
while(1){
print(i);
cin >> c;
if(c == '>')
now ++;
else if(c == '<'){
now --;
break;
}
else if(c == '='){
if(now > la)
ans = i, la = now;
break;
}
}
}
return ans;
}
void dfs(int l, int r){
if(l == r)
return;
int mid = (l + r) >> 1, cnt1 = 0, cnt2 = 0;
while(now < mid){
print(ma);
cin >> c;
now ++;
}
while(now > mid){
print(mi);
cin >> c;
now --;
}
for(int i = l; i <= r; i ++){
print(a[i]);
cin >> c;
if(c == '<'){
a1[++cnt1] = a[i];
print(ma);
cin >> c;
}
else if(c == '=')
a1[++cnt1] = a[i];
else{
a2[++cnt2] = a[i];
print(mi);
cin >> c;
}
}
for(int i = l; i <= l + cnt1 - 1; i ++)
a[i] = a1[i - l + 1];
for(int i = l + cnt1; i <= r; i ++)
a[i] = a2[i - l - cnt1 + 1];
dfs(l, mid);
dfs(mid + 1, r);
return;
}
signed main(){
cin >> t;
while(t --){
sum = 0;
cin >> n;
ma = makeMax();
mi = makeMin();
for(int i = 1; i <= n; i ++)
a[i] = i;
while(1){
print(mi);
cin >> c;
if(c == '=')
break;
}
now = 1;
dfs(1, n);
int ans[n + 5] = {0};
for(int i = 1; i <= n; i ++)
ans[a[i]] = i;
cout << "! ";
for(int i = 1; i <= n; i ++)
cout << ans[i] << " ";
cout << "\n";
cout.flush();
}
return 0;
}

浙公网安备 33010602011771号