2022牛客蔚来杯第九场ABEI(G)

2022牛客蔚来杯第九场

A.Car Show

  • 题意

    • 给定长度为n的数组,问多少个区间内含有1~m的排列
  • 题解

    • 假设找到一个区间[l,r]包含1~m的排列,那么[l,r+1],[l,r+2]…[l,n]都是符合要求的,即从前往后从短到长,每当找到一个符合要求的区间答案+n-r。因此,题目转换为寻找长度尽可能小且含有1~m的区间个数。
    • 上述即为维护一个区间段的题目,适用于双指针算法。固定右指针i,[j,i]维护的是以i为右端点的恰好含有1~m的区间。
  • 代码

#include <iostream>

using namespace std;
#define int long long
const int N=1e5+10;

int n,m,a[N];
int res,cnt[N];//记录答案个数,记录区间内含有i的数量cnt[i]

signed main() {
    cin.tie(0)->sync_with_stdio(false);
    
    cin>>n>>m;
    for(int i=0;i<n;i++) cin>>a[i];
    
    int kinds=0,j=0;//区间内含有的数的种类,左指针j
    for(int i=0;i<n;i++) {
        if(!cnt[a[i]]) kinds++;
        cnt[a[i]]++;
        
        if(kinds==m) {//区间内恰好有1~m
            while(j<i && cnt[a[j]]>1) res+=n-i,cnt[a[j]]--,j++;//移动左端点
            res+=n-i,cnt[a[j]]--,j++,kinds--;//移除左端点,准备找新的区间
        }
    }
    cout<<res<<'\n';
    
    return 0;
}

B.Two Frogs

  • 题意

    • 给定n个桩子,a[i]表示从i桩可跳到的桩子范围为[ i+1, a[i] ]
    • 问两只青蛙分别以同样的步数到达n桩子的概率
  • 题解

    • 两只青蛙是独立的,所以只需先算一只青蛙的概率,即先计算i步到n的概率,答案为下式
      ∑ i = 1 n − 1 p ∗ p \sum_{i=1}^{n-1}p*p i=1n1pp

    • 由题易得,每一种情况的概率都与之前的选择相关,所以为概率dp

      定义:f[i,j]表示走i步到达j号桩子的概率
      转移:对于f[i,j],j号桩子可以到达的所有桩子概率都乘以1/a[j]
      即f[i+1][j+1,j+2...j+a[j]]+=f[i][j]*(1/a[j])
      
    • 状态转移复杂度为n3,所以要优化。枚举状态需要n2无法改变了,而状态转移这一维可以优化,原先为区间操作O(n),可以利用差分优化成O(1)。即把嵌套在第二层循环里的第三层循环,提到第二重循环外面,与第二重循环并列

  • 代码

#include <iostream>

using namespace std;
typedef long long LL;
const int N=8010,mod=998244353;

int n,a[N],inv[N];
int d[N][N],f[N][N];//差分,状态概率数组

int qmi(int a,int b) {
    int ans=1;
    while(b) {
        if(b&1) ans=(LL)ans*a%mod;
        b>>=1;
        a=(LL)a*a%mod;
    }
    return ans;
}

int main() {
    cin.tie(0)->sync_with_stdio(false);
    
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i],inv[i]=qmi(a[i],mod-2);//逆元预处理好,小优化
    
    f[0][1]=1;//0步到1号桩子概率为100%
    for(int i=0;i<n;i++) {//枚举步数,最多n-1步跳到n桩子
        for(int j=1;j<=n;j++) {//前缀和计算出f[i][j],因为转移要用
            d[i][j]=(d[i][j]+d[i][j-1])%mod;
            f[i][j]=(f[i][j]+d[i][j])%mod;
        }
        for(int j=i;j<n;j++) {//转移,i步最少到了i号桩子;差分处理区间转移
            int x=(LL)f[i][j]*inv[j] % mod;
            d[i+1][j+1]=(d[i+1][j+1]+x)%mod;
            d[i+1][j+1+a[j]]=(d[i+1][j+1+a[j]]-x+mod) % mod;
        }
    }
    
    LL res=0;
    for(int i=1;i<n;i++) res=(res+(LL)f[i][n]*f[i][n]%mod) % mod;
    cout<<res<<'\n';
    
    return 0;
}

E.Longest Increasing Subsequence

  • 题意

    • 构造一个长度不超过100的排列,使得其最长上升子序列数量恰好为m个
  • 题解

    • 为了保证 LIS 的个数,一个基础的构造是 2,1,4,3,6,5⋯ ,2n,2n−1这样可以使得 LIS 的个数为 2^n。同时此时的 LIS 序列长度为 n。最后添加一个最大值,作为全部 LIS 的终止。接下来考虑利用二进制拆分,如何填补更小的2^i。
    • 一个可行的操作是,从大到小的考虑 2^i,从后往前的往初始序列中插入。在第 2i+1个数字的前面插入一个比 2n大的数字,记为 x,这样就可以让该数字可以在前面有 2^i 种选择,同时若选择了新加入的数字,则后面只能选最大值。但是选择到了这里 LIS 的长度不够,所以需要在 x 的往后增补一些数字。注意到对于 2^j 的考虑,一定是在 2^i , i>j 之后考虑的。因而 2^j 这里的 LIS 长度增补可以利用后面增补的数字,只需要增补到上升子序列为 n 的长度即可。因而若 2^i 这里已经加了 x 个数字,2^j 这里只需要额外增补 n−j−x 个数字即可。
    • 例如,考虑 (100101)2,那么首先构造2,1,4,3,6,5,8,7,10,9,12,11。首先插入一个最大的:2,1,4,3,6,5,8,7,10,9,12,11,100,考虑 2^2,因而在 6 前面插入一个比最大值小的数字,例如 99。但是这样取到 99 LIS 长度不足,因而还需要增补四个数字(为了全部整数因而要将 99 适当下调为 96):2,1,4,3,(96,97,98,99),6,5,8,7,10,9,12,11,100。最后的 2^0 需要在序列的开头增补一个数字,同时为了满足 LIS 的长度, 因而需要开头加入两个数字:(94,95),2,1,4,3,(96,97,98,99),6,5,8,7,10,9,12,11,100。最后再离散化一下就可以得到最终的排列。
    • 这样的排列长度为:2×30+30=90<100,满足条件,因为总的添加数字个数仅为 LIS 的长度。
#include<iostream>
#include<vector>

using namespace std;

int main() {
    int t;cin>>t;
    while(t--) {
        int m;cin>>m;
        
        int len=30;
        while(!(m>>len & 1)) len--;//分块数量=2进制位数
        vector<int> add(len,0);
        int cnt=0;//新增的总共数量
        for(int i=len-1;i>=0;i--)//从大到小找插入的位置和数量
            if(m>>i & 1) {//为1的位置需要插入
                add[i]=len-i-cnt;//插入的数量
                cnt+=add[i];
            }
        
        printf("%d\n",2*len+cnt+1);//长度为基本的len块,每块有2个,以及插入的cnt个,还有末尾插入的一个
        int base=2*len;//插入的数值
        for(int i=0;i<len;i++) {
            while(add[i]--) printf("%d ",++base);//输出添加插入的数
            printf("%d %d ",2*i+2,2*i+1);//输出块
        }
        printf("%d\n",++base);
    }
    
    return 0;
}

I.The Great Wall II

  • 题意

    • 给定n个数,分成k(1,2…n)段,每段代价取其段中最大值,求k=1,2…n段的代价最小值
  • 题解

    • 因为每多分一段,都与前一段相关,所以dp,朴素版:O(n^3)

      定义:f[i,j]表示前j个数分成i段的最小代价
      转移:枚举段数i和终点j,转移起点计算答案,代码如下
      
      for(int i=2;i<n;i++) {
      	for(int j=i;j<=n;j++) {
      		for(int k=j;k>=1;k--) {
      			f[i][j]=min(f[i][j],f[i-1][k-1]+mx(a[k,k+1,...j]))
      		}
      	}
      }
      
    • 单调栈优化
      f [ i , j ] = m i n ( f [ i , j ] , f [ i − 1 ] [ k ] + m a x ( a k + 1 , a k + 2 , . . . a j ) ) f[i,j]=min(f[i,j],f[i-1][k]+max(a_{k+1},a_{k+2},...a_j)) f[i,j]=min(f[i,j],f[i1][k]+max(ak+1,ak+2,...aj))

      • 我们用单调栈维护关于a[i]的一个元素单调递减的指针栈,那么栈中的两相邻元素k,j构成的区间[k+1,j]一定都是不减的,即维护了所有以a[j]结尾的能影响到最大值的一个区间,即在此区间内最大值一定是a[j]。搞定了转移方程的max部分
      • 该单调栈同时再维护min( f[i-1] )的信息,(当i=2时,是分为两段,min(f[i-1])即为只有一段的最小代价,取这个代价最小一段作为一段,同时取新加入的a[j]所影响的区间作为另一段,即构成了两段;i为其他数同样理解)。搞定了f[i-1,k]部分
      • 单调栈同时维护minall( f[i] ),在枚举i段时,有多种划分成k段的以某数结尾的最优方案,所以对于所有的minall取最小值
    • 滚动数组优化,同时i=1时特殊处理,因为没法i-1

  • 代码

#include <iostream>
#include <cstring>
#include <algorithm> 
using namespace std; 
const int N = 8010, INF = 1e9; 

int n, stk[N], top, a[N]; 
int f[2][N]; 
int minf[N], minall[N]; 

int main() { 
    cin >> n; 
    for(int i = 1 ; i <= n ; i ++ ) cin >> a[i]; 
    for(int i = 1 ; i <= n ; i ++ ) f[1][i] = max(f[1][i - 1], a[i]); 
    cout << f[1][n] << endl; 

    for(int i = 2 ; i <= n ; i ++ ) { 
        top = 0; minall[0] = INF; 
        for(int j = i ; j <= n ; j ++ ) { 
            int temp = f[(i - 1) & 1][j - 1]; // 添加的数为a[j],代价之一为f[i-1][j-1]
            while(top && a[stk[top]] <= a[j]) temp = min(temp, minf[top -- ]); // 合并区间 
            stk[++ top] = j; // 新区间 
            minf[top] = temp; // 更新新区间的最小f[i - 1][k] (k在区间范围内) 
            minall[top] = min(minall[top - 1], minf[top] + a[j]); // 更新最小前缀 
            f[i & 1][j] = minall[top]; 
        } 
        cout << f[i & 1][n] << endl; 
    }
    
    return 0;
 }

G.Magic Spells

  • 题意

  • 题解
    *

  • 代码


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值