CSP 模拟 4

今日推歌:

  1. Serenade in G ‘Eine kleine Nachtmusik’ K525 - Wolfgang Amadeus Mozart

今天比赛直接搬的 ARC 125,126 的 CD 题,那这样我也能出模拟赛(

但是为什么 HZOI2022 都不写比赛题解,差评

今天被 HZOI2023,2024 薄纱,我直接退役得了

T1 最长上升子序列

考虑一个明显字典序不是最小但是满足条件的构造:

\[(a_1,a_1-1,\cdots,1),(a_2,a_2-1,\cdots,a_1+1),\cdots,(n,n-1,\cdots,a_{n-1}+1) \]

每一组只会向后一组贡献 \(1\) 的最长上升子序列长度,但是这个字典序明显不是最小的,但是可以根据这种构造改进。

构造步骤为:

  1. 一开始将 \([1,a_1)\) 加入集合 \(B\)
  2. \(a_i\) 加入构造的序列。
  3. 取出当前集合 \(B\) 中的最小数字加入序列,如果没有则不加入。
  4. \((a_i,a_{i+1})\) 加入集合 \(B\)
  5. 重复 \(2\sim4\) 步直到 \(i=k\)
  6. \([a_k,n]\) 加入集合 \(B\)
  7. 将集合 \(B\) 中的元素倒序加入序列。

因为是每次取 \(B\) 中最小的数,所以能保证字典序最小,对于 \(a_i\)\(a_{i+1}\) 的每一个数因为是倒序的,一共有 \(k\) 组这样的数,所以能保证最长上升子序列长度为 \(k\)

点击查看代码
#include<bits/stdc++.h>
#define N 200005
using namespace std;

int n,k,a[N],b[N],tot,now,num[N];

int main(){
    cin>>n>>k;
    for(int i=1;i<=k;i++)
        scanf("%d",a+i);
    for(int i=1;i<a[1];i++) b[++tot]=i;
    now=1;
    for(int i=1;i<k;i++){
        if(now<=tot) num[i]=b[now++];
        for(int j=a[i]+1;j<a[i+1];j++) b[++tot]=j;
    }
    for(int i=1;i<k;i++){
        printf("%d ",a[i]);
        if(num[i]) printf("%d ",num[i]);
    }
    for(int i=n;i>=a[k];i--)
        printf("%d ",i);
    for(int i=tot;i>=now;i--)
        printf("%d ",b[i]);
}

T2 独特序列

定义 dp 状态 \(f_{i}\) 为以 \(i\) 为结尾的独特序列个数,考虑转移。

\(lst_i\) 为上一次出现数 \(i\) 的地方,则 \(a_i\) 的转移不能到 \(lst_{a_i}\) 及之前,因为这样的话就会有并不独特的序列更新,而且在 \(a_i\) 更新之后,\(lst_{a_i}\) 不再产生贡献,因为只需要最后一个用来转移即可,所以转移方程如下:

\[f_i=\sum_{j=lst_{a_i}+1}^{a_i-1}f_j \]

因为要消去影响,所以每次转移之后 \(f_{lst_{a_i}}=0\)

如果直接转移的话复杂度是 \(O(n^2)\) 的,然后发现转移方程可以用树状数组维护,时间复杂度减为 \(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
#define N 200005
#define int long long
using namespace std;

const int p=998244353;
int n,a[N],f[N],lst[N],ans;
struct FenwickTree{
    int c[N];
    int lowbit(int x){return x&(-x);}
    void update(int x,int val){if(!x) return; for(int i=x;i<=n;i+=lowbit(i)) c[i]=(c[i]+val+p)%p;}
    int query(int x){int ans=0;for(int i=x;i;i-=lowbit(i)) ans=(ans+c[i])%p;return ans;}
}T;

signed main(){
    cin>>n;
    for(int i=1;i<=n;i++) scanf("%lld",a+i);
    for(int i=1;i<=n;i++){
        if(!lst[a[i]]) f[i]=1;
        f[i]=(f[i]+(T.query(i-1)-T.query(max(0ll,lst[a[i]]-1))+p)%p)%p;
        T.update(i,f[i]);T.update(lst[a[i]],-f[lst[a[i]]]);lst[a[i]]=i;
    }
    for(int i=1;i<=n;i++)
        ans=(ans+f[lst[i]])%p;
    cout<<ans;
}

T3 最大GCD

如果能把数列的所有数补到最大值的话,则说明直接向上加即可,直接输出。

如果这样的话说明 \(\gcd(a_1,a_2,\cdots,a_n)\le \max a_i\)

如果想让数列的 \(\gcd\)\(x\) 整除的话,说明数列的每一项都被 \(x\) 整除,所以其最小操作次数为:

\[\sum_{i=1}^{n}x-(a_i\bmod x) \]

如果这个次数 \(\le k\) 的话,说明 \(x\) 是满足条件的 \(\gcd\)

但是答案并没有单调性,考虑展开这个式子:

\[\begin{aligned} \sum_{i=1}^{n}x-(a_i\bmod x)&=nx-\sum_{i=1}^{n}(a_i\bmod x)\\ &=nx-\sum_{i=1}^{\max a_i}(i\bmod x)cnt_i \end{aligned} \]

其中 \(cnt_i\) 为数 \(i\) 出现的次数,后面的 \((i\bmod x)\) 是以 \(x\) 为周期的,所以考虑维护一个 \(\displaystyle f_n = \sum_{i=1}^{n} i\cdot cnt_i\) 和一个 \(\displaystyle g_n = \sum_{i=1}^{n} cnt_i\),然后对于每个 \(((k-1)x,kx]\) 来说它的贡献为 \(kx\cdot (g_{kx}-g_{(k-1)x})-(f_{kx}-f_{(k-1)x})\) 求解即可,时间复杂度为 \(O(nH_n)=O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
#define N 600005
#define int long long
using namespace std;

int n,k,a[N],Gcd,maxn,sum,cnt[N],f[N],g[N];

signed main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++)
        scanf("%lld",a+i),maxn=max(a[i],maxn),cnt[a[i]]++;
    for(int i=1;i<=N-5;i++) f[i]=f[i-1]+cnt[i],g[i]=g[i-1]+cnt[i]*i;
    for(int i=1;i<=n;i++) sum+=maxn-a[i];
    if(sum<=k){
        cout<<maxn+(k-sum)/n;
        return 0;
    }
    for(int i=maxn;i>=1;i--){
        int tmp=0;
        for(int j=1;(j-1)*i<=maxn;j++)
            tmp+=(f[i*j-1]-f[i*(j-1)])*i*j-(g[i*j-1]-g[i*(j-1)]);
        if(tmp<=k){
            printf("%lld\n",i);
            return 0;
        }
    }
}

T4 连续子段

一眼状压,两眼不会做

首先可以考虑对于最后组成连续 \(k\) 个数的 \(k\) 项,一定是先聚集在一个地方然后冒泡排序,所以这个交换次数可以拆成聚集在一起需要的次数和冒泡排序的次数(也就是逆序对个数)。

设状态 \(f_{i,S}\) 表示考虑到第 \(i\) 项,最终的 \(k\) 个数情况为 \(S\),则我们对于每个数有两种转移方式:

  1. \(i\) 作为最后的 \(k\) 个数出现,其贡献为 \(i\) 作为末尾的逆序对数目
  2. \(i\) 不作为最后的 \(k\) 个数出现,那么因为最后 \(k\) 个数要聚集在一起,所以要不就是 \(i\) 左边的最终项全到 \(i\) 右边,要么就是反过来,所以这个贡献为 \(\min(\operatorname{popcount}(S),k-\operatorname{popcount}(S))\)

然后发现倒序枚举 \(S\) 可以滚掉一位,时间复杂度为 \(O(2^kn)\)

点击查看代码
#include<bits/stdc++.h>
#define N 205
#define bitcnt __builtin_popcount
#define inf 0x3f3f3f3f
using namespace std;

int n,k,a[N],f[1<<16],ans=inf,range;
vector<int> vec[N];

int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++) scanf("%d",a+i),a[i]--;
    memset(f,0x3f,sizeof(f));
    f[0]=0;range=(1<<k)-1;
    for(int i=1;i<=n;i++){
        for(int j=range;j>=0;j--){
            int cnt=bitcnt(j);
            if(!(j&(1<<a[i]))) f[j|(1<<a[i])]=min(f[j|(1<<a[i])],f[j]+bitcnt(j&(~((1<<a[i])-1))));
            f[j]+=min(cnt,k-cnt);
        }
    }
    cout<<f[range];
}
posted @ 2023-07-24 18:03  Rolling_star  阅读(52)  评论(1编辑  收藏  举报