Codeforces Global Round 19
G.Birthday
题目描述
解法
可以写个暴力跑出 \(n=3,4,5,6,7...\) 的情况,猜测最后的结果是最小的 \(x\) 满足它是 \(2\) 的幂次且 \(x\geq n\)
若 \(n=x\),则可以让 \(n\leftarrow n-1\),这样答案不变,并且可以保证 \(n<x\)
有一个简单的构造想法是把 \(i(i>\frac{x}{2})\) 和 \(x-i\) 配对,就可以得到 \(x\) 和 \(2i-x\),我们把能配对的都配对了,留下得到的若干个 \(x\),会剩下这些东西:
- \(1,2...n-x-1\)
- \(2,4...2\cdot (n-\frac{x}{2})\)
- 单独的 \(\frac{x}{2}\)
设原来的问题为 \((n,x,k)\),其中 \(k\) 表示每个数乘上的倍数,那么我们得到了 \((n-x-1,\frac{x}{2},k)\) 和 \((n-\frac{x}{2},\frac{x}{2},2k)\) 这两个子问题,可以直接递归下去。但是递归回来后还有一个问题,就是要把 \(\frac{x}{2}\) 变成 \(x\) 才能回溯。
发现对于 \(n\leq 7\) 的情况,最后一步都是 \((0,x)\),而 \(0\) 可以完成下面神奇的操作:
- 操作 \((0,a)\),变成了 \((a,a)\)
- 操作 \((a,a)\),变成了 \((0,2a)\)
也就是 \(0\) 在保证自身不变的情况下,可以倍增一个数。那么我们的问题都撤销最后一步,这样就可以留下一个 \(0\)
再来梳理一遍,对于 \(n\leq 7\) 我们直接取用暴搜的结果;对于 \(n\geq 8\),我们可以直接递归下去,由于一定有一个子问题满足 \(n'\geq 3\),所以回溯时一定能获取一个 \(0\),用这个 \(0\) 倍增所有 \(<x\) 数就可以回溯了。
操作次数 \(O(n\log n)\),讲解没完全看懂的可以参考代码。
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
#define pii pair<int,int>
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
vector<pii> b[8] = {
{},
{},
{},
{{1, 3}, {2, 2}},
{{1, 3}, {2, 2}},
{{1, 2}, {1, 3}, {2, 4}, {2, 6}, {4, 4}, {3, 5}, {0, 2}, {2, 2}, {0, 4}, {4, 4}},
{{1, 2}, {1, 3}, {2, 4}, {2, 6}, {4, 4}, {3, 5}, {2, 6}, {0, 4}, {4, 4}},
{{1, 2}, {3, 5}, {2, 6}, {4, 4}, {1, 0}, {1, 3}, {1, 7}, {2, 6}, {4, 4}}
};
int T,n,m;vector<pii> a;
void add(int x,int y,int k)
{
a.push_back({x*k,y*k});
}
void get(int x,int to,int k)
{
while(x<to)
{
add(0,x,k);
add(x,x,k);
x<<=1;
}
}
void dfs(int n,int x,int k)
{
if(n==x) {dfs(n-1,x,k);return ;}
if(n<=x/2)
{
dfs(n,x/2,k);
for(int i=1;i<n;i++) get(x/2,x,k);
return ;
}
if(n<=7)
{
for(auto [x,y]:b[n]) add(x,y,k);
return ;
}
for(int i=x/2+1;i<=n;i++) add(x-i,i,k);
int A=x-n-1,B=n-x/2;
if(A>=3)
{
dfs(A,x/2,k);
for(int i=1;i<A;i++) get(x/2,x,k);
}
if(B>=3) dfs(B,x/2,k*2);
get(x/2,x,k);
if(A<=2) for(int i=1;i<=A;i++) get(i,x,k);
if(B<=2) for(int i=1;i<=B;i++) get(i,x/2,k*2);
if(A>=3 && B>=3) add(0,x,k);
}
void work()
{
n=read();m=1;a.clear();
if(n==2) {puts("-1");return ;}
while(m<n) m<<=1;
if(n<=7) a=b[n];
else dfs(n,m,1);
add(0,m,1);
printf("%d\n",a.size());
for(auto [x,y]:a) printf("%d %d\n",x,y);
}
signed main()
{
T=read();
while(T--) work();
}
H.Minimize Inversions Number
题目描述
解法
首先考虑 \(k=1\) 的情况,设 \(d_i=\sum_{j=1}^{i-1} [p_j>p_i]-[p_j<p_i]\),表示如果前移 \(i\) 逆序对的减少量。
对于 \(k>1\) 的情况,直接用选取点的 \(\sum d_i\) 计算代价是不行的,需要减去一个「选取子序列的顺序对数 \(-\) 逆序对数」的修正量。设 \(s_1,s_2...s_k\) 表示选取的子序列,\(q_1,q_2...q_k\) 表示对应的值,那么减去的总量是:
所以我们要最大化 \(\sum_{i=1}^k d_{s_i}-2\sum_{i=1}^k\sum_{j=1}^{i-1}[q_j>q_i]\)
关键的结论是:对于原序列的逆序对 \((i,j)\),如果 \(i\) 在子序列中,那么 \(j\) 一定要在子序列中。
证明使用反证 + 调整法,即证明存在方案不劣于 \(i\) 在子序列中,但是 \(j\) 不在子序列中。
我们从 存在逆序对 \((i,j)\),满足 \(i\) 在子序列中,但是 \(j\) 不在子序列中 开始调整。考虑收紧限制,找到 \(j-i\) 最小并且满足上述条件的逆序对 \((i,j)\),因为 \(j-i\) 最小,所以在 \((i,j)\) 之间不存在这样的数:
- 值介于 \(p_i\) 和 \(p_j\) 之间的数。
- 大于 \(p_i\) 且在子序列中的数。
- 小于 \(p_j\) 且不在子序列中的数。
考虑取消 \(i\) 的选择,转而选择 \(j\),考虑代价的变化,对于某个位置 \(k\) 的贡献是:
- 如果 \(k>j\) 且不在子序列中,由于相对位置不变,所以无贡献。
- 如果 \(k>j\) 且在子序列中,当且仅当 \(p_k\) 介于 \(p_i,p_j\) 之间会有 \(-2\) 的贡献,否则无贡献。
- 如果 \(k<i\) 且在子序列中,由于相对位置不变,所以无贡献。
- 如果 \(k<i\) 且不在子序列中,当且仅当 \(p_k\) 介于 \(p_i,p_j\) 之间会有 \(-2\) 的贡献,否则无贡献。
- 如果 \(i<k<j\),\(p_k>p_i\) 且不在子序列中,贡献为 \(-1\)
- 如果 \(i<k<j\),\(p_k<p_j\) 且在子序列中,贡献为 \(-1\)
所以总贡献非正,并且由于调整可以在有限步内结束,所以 \(i\) 在子序列中,但是 \(j\) 不在子序列中 不会成为最优解。
如果感性理解上面的结论,就是决定逆序对的只有:位置和值。那么位置大的并且值小的放前面更优,所以原序列的逆序对会导致一些选择的偏序关系,可以指向上面的结论。
知道了这个结论后,我们只需要最大化 \(\sum_{i=1}^k d_{s_i}-2\sum_{i=1}^k\sum_{j=s_i+1}^n [p_i>p_j]\)
所以预处理 \(c_i=d_i-2\sum_{j=i+1}^n [p_i>p_j]\),然后按照 \(c\) 从大到小选择即可,时间复杂度 \(O(n\log n)\)
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 500005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,ans,a[M],b[M],c[M];
void add(int x)
{
for(int i=x;i<=n;i+=i&(-i)) b[i]++;
}
int ask(int x)
{
int r=0;
for(int i=x;i>0;i-=i&(-i)) r+=b[i];
return r;
}
void work()
{
n=read();ans=0;
for(int i=1;i<=n;i++) a[i]=read(),b[i]=0;
for(int i=1;i<=n;i++)
{
int d=ask(a[i]);
ans+=i-1-d;c[i]=i-1-2*d;
add(a[i]);
}
for(int i=1;i<=n;i++) b[i]=0;
for(int i=n;i>=1;i--)
c[i]-=2*ask(a[i]),add(a[i]);
sort(c+1,c+1+n,greater<int>());
printf("%lld ",ans);
for(int i=1;i<=n;i++)
{
ans-=c[i]+(i-1);
printf("%lld ",ans);
}
puts("");
}
signed main()
{
T=read();
while(T--) work();
}