2022牛客蔚来杯第九场
文章目录
- 2022牛客蔚来杯第九场
- [A.Car Show](https://ac.nowcoder.com/acm/contest/33194/A)
- [B.Two Frogs](https://ac.nowcoder.com/acm/contest/33194/B)
- [E.Longest Increasing Subsequence](https://ac.nowcoder.com/acm/contest/33194/E)
- [I.The Great Wall II](https://ac.nowcoder.com/acm/contest/33194/I)
- [G.Magic Spells](https://ac.nowcoder.com/acm/contest/33194/G)
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=1∑n−1p∗p -
由题易得,每一种情况的概率都与之前的选择相关,所以为概率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[i−1][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
-
题意
-
题解
* -
代码