【Codeforces】【Div2】1068(cf 2173)

A

暴力

B

按最大最小值从前往后dp即可

C

题意:给你一个长度为 \(n\) 的序列 \(a\) 和一个数 \(k\) ,序列的数满足 \(1\le a_i \le k\) 。现在要求你求出一个最小的集合 \(B\) ,要求:每个 \(a_i\) 至少一个因数在 \(B\) 里面且对于 \(B\) 中的每个数 \(b_j\) 其小于等于 \(k\) 的倍数(包含 \(b_j\) 自身)都要在序列 \(a\) 中出现。请输出这个集合,或者输出 -1 表明不存在这个集合。数据范围:\(1\le n \le 2\cdot 10^5\) \(1 \le k \le 10^9\)

题解:赛时直接敲了个显然的做法,然后就过了。将 \(a\) 从小到大排序,然后对于每个 \(a_i\) 如果它没被之前的数遍历过(即不是 \(B\) 中数的倍数)就将其加入 \(B\) 中,反之跳过。同时每加入一个数就遍历到 \(k\) 的倍数,判断其是否在 \(a\) 中,如果在就将该数标记以便后续跳过,否则则是不能构造 \(B\) 输出 -1。

实际上这个做法的速度非常快,这里本人并没有一个很好的求出理论时间复杂度的方法。看了下 cf 的题解,好像也没有严格地提出一个理论复杂度。

代码:

#include<bits/stdc++.h>
using namespace std;

#define LL __int128
#define ll long long
#define PLL pair<ll,ll>
const ll N=2e5+10,mod=998244353,INF=2e18;
ll T,n,k;

void solve(){
    cin>>n>>k;
    vector<ll> a(n+1);
    for(int i=1;i<=n;i++) cin>>a[i];
    unordered_map<ll,ll> h;
    sort(a.begin()+1,a.end());
    for(int i=1;i<=n;i++) h[a[i]]++;
    vector<ll> res;
    for(int i=1;i<=n;i++){
        if(h[a[i]]!=-1){
            res.push_back(a[i]);
            for(ll j=1;j*a[i]<=k;j++){
                ll t=j*a[i];
                if(h[t]==0){
                    cout<<-1<<endl;
                    return;
                }
                h[t]=-1;
            }
        }
    }

    cout<<res.size()<<endl;
    for(auto t:res) cout<<t<<" ";
    cout<<endl;
}

int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    T=1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

D

题意:给你一个数 \(n\) ,每一轮你可以对这个数加 \(2^l\)\(l \ge 0\) 。一共 \(k\) 轮,如果这个数在你加后在二进制下产生了进位,你获得产生进位数量个数的分数。请输出对于每组数据,你能得到最大的分数。数据范围:\(1\le t \le 1000\) (数据数量)\(1\le n < 2^{30}\)\(0\le k \le 10^9\)

题解:考虑当 \(n\) 等于 0 的情况,一个显然的想法是加出一段连续的1,在最后一轮加到结尾,然后共有 \(k-1\) 次进位,得分是 \(k-1\) 。考虑是否有更优的做法,显然没有。因为必须需要一个 1 来使得剩下的数开始进位,这是还剩下 \(k-1\) 次操作,而每次操作只能加一个 1,一次操作显然不可能有大于 1 的贡献。所以这就是最优做法。

现在考虑 \(n\ne0\) 的情况:假设 \(n\) 的二进制形式下的最高位为第 \(mx\) 位(这里是 1 下标,即第一位指 \(2^0\) 那一位),且有 \(m\) 个 1,那么就有 \(mx-m\) 个 0。假设 \(k \ge mx-m+1\) ,则我们可以将从 1 到 \(mx\) 的二进制下标全填成 1。剩下的做法也即 \(n=0\) 的情况:当还剩下大于 1 轮时,每次最高位加 1。只剩下 1 轮时,往最低位的 1 加 1 以启动进位,共有 \(k-1+m\) 次进位。假设 \(k < mx-m+1\),那么此时我们就不能将从 1 到 \(mx\) 位全部加成 1 并且加上一个数以便产生进位。显然此时 \(k\le 30\),且位数也只有 30 位,所以可以 dp 解决该问题。

设置 dp 状态:\(dp[i][j][0/1]\) 代表从最低位到第 \(i\) 位用了 \(j\) 次操作且这一位为 \(0/1\) 时得到的最大分数。dp 转移主要有两种,一种是从第 \(i-1\) 位转移,一种是从 \(i\) 位进行操作,一直到第 \(id\) 位,使得第 \(id\) 位接受从 \(i\)\(id-1\) 产生的进位。转移过程较为简单,我认为直接看下面的代码就能看懂。

代码:

#include<bits/stdc++.h>
using namespace std;

#define LL __int128
#define ll long long
#define PLL pair<ll,ll>
const ll N=2e5+10,mod=998244353,INF=2e18;
ll T,n,k;

void solve(){
    cin>>n>>k;
    vector<ll> a(n+1);
    for(int i=1;i<=n;i++) cin>>a[i];
    unordered_map<ll,ll> h;
    sort(a.begin()+1,a.end());
    for(int i=1;i<=n;i++) h[a[i]]++;
    vector<ll> res;
    for(int i=1;i<=n;i++){
        if(h[a[i]]!=-1){
            res.push_back(a[i]);
            for(ll j=1;j*a[i]<=k;j++){
                ll t=j*a[i];
                if(h[t]==0){
                    cout<<-1<<endl;
                    return;
                }
                h[t]=-1;
            }
        }
    }

    cout<<res.size()<<endl;
    for(auto t:res) cout<<t<<" ";
    cout<<endl;
}

int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    T=1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

E

题意:给你一个长为 \(n\) 的排列 \(p\),你有一种操作:输出两个位置 \(x\)\(y\)\(x\ne y\)),然后交互器会有 0.5 的概率返回 \(x\)\(y\) ,有 0.5 的概率返回 \(n-x+1\)\(n-y+1\) 。设交互器返回的两个值为 \(a\)\(b\) ,其含义是交互器会交换 \(p_a\)\(p_b\) 的值。现在你需要使用这个操作将这个排列 \(p\) 排序为从小到大的排列。当完成排序后,输出 \(!\) 。你最多能进行 \(\lfloor 2.5n+800 \rfloor\) 次操作。

题解:我们看这个操作,这个操作更为直白的翻译是,你有一半的概率交换位置 \(x\)\(y\) 的数并有另一半概率交换与 \(x\)\(y\) 在这个序列上以序列中心轴对称的位置。(这个翻译可能过于抽象,可以在纸上自己手玩一下,重点在于指明这两种交换的方法是轴对称的)考虑如果我们会有 1 的概率交换 \(p_x\)\(p_y\) ,即我们说啥交互器就换啥。在这种情况下,很显然我们每次从小往大的换就可以了。例如:如果第一个数不是 1,那么我们就找 1 在哪,将其与位置 1 的数交换即可,然后从前往后的换即可。如果我们直接把这种方式照搬到本题呢?假设现在你要换第 \(x\) 位和第 \(y\) 位的数(\(x<y\)),此时 1 到 \(x-1\) 位的数已经排好了。如果 \(n-x+1\ge x\)\(n-y+1\ge x\) ,那么这两种交换都不会影响我们已经排好的数,这当然是最好的情况。但是显然 \(n-x+1\ge x\)\(n-y+1\ge x\) 这种条件不是能一直成立的,所以此时如果交互器交换 \(n-x+1\)\(n-y+1\) 的位置的数,那么必然会打乱我们之前已经排序好的数。所以这种方式是不可行的。所以我们需要一种方式,使得两种交换方式不会打乱我们已经排序好的数或只能打乱很少的位置。因为前面我们知道这两种交换方法是轴对称的,所以这启发我们也已轴对称的形式从两边往里交换。例如:我们先交换 1,交换完 1 后不是交换 2 而是交换 n ,这样从外往里交换知道最中间。这样的一个好处是每次交换只会打乱当前轴对称的两个位置,而不会打乱之前已经排序好的位置。根据数学直觉(其实是没时间了直接写一发),交换上限应该是够的,然后写一发就成功 AC 了。

虽然题 AC 了,但我们还是要知其所以然。所以现在进行证明:假设现在我们要排序的对称的数为 \(x\)\(y\) ,设 \(E(1)\) 代表使得其中一个数排序完成的期望次数,\(E(2)\) 代表两个数都排序完成的期望次数。现在我们求 \(E(1)\) 。$$ E(1)=\frac{1}{2}+\frac{2}{4}+\frac{3}{8}+...+\frac{n}{2^n} $$怎么得出上述式子呢?进行一次操作时,有 0.5 的概率直接排序完成,反之我们需要进行第二次操作,这时在第一次操作的基础上我们有 0.5 的概率排序完成(即 \(\frac{2}{4}\) 的得来),一直计算下去就得到了上述式子。考虑如何求上述式子,我们有 $$ 2E(1)=\frac{1}{1}+\frac{2}{2}+\frac{3}{4}+...+\frac{n}{2^{n-1}} $$
然后两式相减得到 $$ E(1) = 1 + \frac{1}{2} + \frac{1}{4} + \frac{1}{8} + \cdots + \frac{1}{2^{n-1}} - \frac{n}{2^n} $$
\(n \rightarrow \infty\) ,前面就是一个等比数列,后面为 0 ,算出 \(E(1)=2\)

考虑怎么算 \(E(2)\) ,根据 \(E(1)\) ,我们可以得出 $$ E(2)= \frac{E(1)+1}{2} + \frac{E(1)+1+E(1)+1}{4} + \frac{E(1)+1+E(1)+1+E(1)+1}{8} + ... + \frac{n \cdot (E(1)+1)}{2^n} $$
\(\frac{E(1)+1}{2}\) 指的是当第一次我们就完成了排序,此时我们共进行了 \(E(1)+1\) 次操作(\(E(1)\) 代表将其中一个数排序好的次数,1 代表一次操作就将另一个数排序好了),\(\frac{E(1)+1+E(1)+1}{4}\) 指第二次我们才完成排序,其中第一个 \(E(1)+1\) 代表第一次我们失败了,使得我们两个数都乱序了,所以我们需要再进行一次 \(E(1)+1\),以此类推。所以我们可以求出 \(E(2)=6\) !也就是说我们上述的式子是错误的!错误在哪里呢?错误就在第二个及之后的 \(E(1)\) 上,假设我们已经把 \(x\) 排序好了,现在我们需要排序 \(y\) ,且我们求出我们给交互器的参数是 \(y,y_2\) 。因为 \(x\)\(y\) 是轴对称的,所以如果交互器没有按照我们所想的排序 \(y,y_2\),那么它就一定将 \(x,n-y_2+1\) 交换了。此时如果我们继续给参数 \(y,y_2\) ,我们就一定能将 \(x\)\(y\) 中的一个数排序完成!因为假设交互器确实交换了 \(y,y_2\) 那么 \(y\) 就交换完成了,反之交互器会交换 \(x,n-y_2+1\) ,即将上次交换的影响消除,\(x\) 回到了其原来的位置(即将 \(x\) 重新排序成功了)。所以第二个及之后的 \(E(1)=1\) !(这里在演草纸上手玩一下会更清晰),所以正确的式子是 $$ E(2) = \frac{E(1)+1}{2} + \frac{E(1)+1+1+1}{4} + \frac{E(1)+1+1+1+1+1}{8} + ... + \frac{E(1)+2 \cdot n-1}{2^n} $$得到 \(E(2)=5\),求的方法和求 \(E(1)\) 的方法是一致的,这里不再赘述。

由此我们得到排序两个对称的数的期望次数为 5 次,所以总共期望次数为 \(2.5n\) 。800 次是给的容错。

代码:

#include<bits/stdc++.h>
using namespace std;

#define LL __int128
#define ll long long
#define PLL pair<ll,ll>
const ll N=2e5+10,mod=998244353,INF=2e18;
ll T,n,k;

PLL ask(ll x,ll y){
    cout<<"? "<<x<<" "<<y<<endl;
    cout.flush();
    ll a,b;
    cin>>a>>b;
    return {a,b};
}

void solve(){
    cin>>n;
    vector<ll> a(n+1),id(n+1);
    for(int i=1;i<=n;i++) cin>>a[i],id[a[i]]=i;
    for(int i=1,j=n;i<=j;i++,j--){
        while(1){
            if(a[i]==i&&a[j]==j) break;
            if(a[i]!=i){
                ll tid=id[i];
                ll t2id=i;
                PLL pa=ask(t2id,tid);
                swap(id[a[pa.first]],id[a[pa.second]]);
                swap(a[pa.first],a[pa.second]);
                continue;
            }
            if(a[j]!=j){
                ll tid=id[j];
                ll t2id=j;
                PLL pa=ask(t2id,tid);
                swap(id[a[pa.first]],id[a[pa.second]]);
                swap(a[pa.first],a[pa.second]);
                continue;
            }
        }
    }
    cout<<"! "<<endl;
    cout.flush();
}

int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    T=1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

F

代补

如有错误,敬请指出,不胜感激!

posted @ 2025-12-06 15:56  Zaria  阅读(6)  评论(0)    收藏  举报
.noShow{display:none!important}