• 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}+{k\choose 2}-2\sum_{i=1}^k\sum _{j=1}^{i-1} [q_j>q_i] \]

    所以我们要最大化 \(\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();
    }
    
  • 相关阅读:
    第一周作业
    C语言I博客作业08
    十四周助教总结
    十三周助教总结
    C语言I博客作业07
    C语言II博客作业01
    学期总结
    C语言I博客作业08(未完成)
    C语言I博客作业07
    C语言I博客作业06
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/16440676.html
Copyright © 2020-2023  润新知