2021.牛客暑假多校 第五场 补题

  1. B题 https://ac.nowcoder.com/acm/contest/11256/B 思维、期望

大意:现有 n 个盒子,里面装有黑球或白球概率均为 1 2 \frac{1}{2} 21 且相互独立,现在要确定盒子中黑球的数量,显然需要把所有盒子都打开。现多了一个选择,可以在开始的时候花费代价c,然后每次开盒前都可以知道剩下盒子里黑球的个数。求最小花费的期望值。

思路:题目又双叒叕读假了。对于此题,显然就两种开盒方案,一是直接开完,二是先花费代价c,再开盒。求两种花费期望的最小值就行了。对于第一种很好求,直接把所有开盒的期望加起来就行了。对于第二种,显然我们开盒先开花费少的,所以我们把花费从小到大排序。首先一定会花费代价c,对于没有开盒直接结束是有可能的,但是没有花费,所以可以直接跳过。对于之后在任何时候都有可能结束,对于期望值显然就是花费前缀和乘以概率。现在来想想,我们什么时候不用开盒直接结束呢?剩下的球的颜色相同且于上一个开的盒子的颜色不同,因为一旦颜色相同我们上一个回合就结束了。所以对于开了 i 个盒子,还剩下 n-i个盒子结束的概率为 1 2 n − i ∗ 2 ∗ 1 2 \frac{1}{2}^{n-i} *2*\frac{1}{2} 21ni221​即 1 2 n − i \frac{1}{2}^{n-i} 21ni​ ,并且对于最后一个盒子是一定不用开的。

总结:好好读题,别一看到期望就想期望dp。

  1. C 题 https://ac.nowcoder.com/acm/contest/11256/C 思维

大意:给定一个长度为 n 的字符串,代表乒乓球对局的得分状态,‘W’ 代表得一分,‘L’代表对方得一分。对于乒乓球比赛,若规定了一个得分 k,先得 k 分的表示赢得了一局,然后进行下一局,直到遍历完所有的字符。除此之外,我们都知道乒乓球是有个“赛点规则”的,即若先得满k分获胜,当双方比分 k:k-1 时是不会结束的,直到双方分差>=2的时候才会分出胜负。对于每个分数k , (1<=k<=n) 记最终赢的局数为 f k f_k fk。 求 ∑ k = 1 n f k ∗ ( n + 1 ) k − 1 \sum_{k=1}^{n}{f_k*(n+1)^{k-1}} k=1nfk(n+1)k1

思路:对于每个分数 i,最起码进行 i 个回合才能完成一局,即局数 为 n i \frac{n}{i} in​,这是个调和级数。如果能O(1)的算出每局的输赢情况,那么我们就可以O(nlogn)的时间复杂度算出结果。所以我们接下来要做的就是怎么想办法O(1)的求出某一局的输赢情况。先不考虑“赛点规则”,假设我们已经完成了若干局,对于在当前在字符串中的位置为 i,我们要完成这一局需要找到最先满足 k 个 W或L 的位置,我们没办法直接预处理,但是可以转换成前缀问题,我们可以先预处理出 前 i 个字符中 W/L 的个数 s u m w i sumw_i sumwi s u m l i suml_i sumli ,以及 第 j 个 W/L 的下标 p o s w j posw_j poswj p o s l j posl_j poslj 这样我们就可以快速找到完成这一局的位置,即 m i n ( p o s w [ s u m w [ i − 1 ] + k ] , p o s l [ s u m l [ i − 1 ] + k ] ) min(posw[sumw[i-1]+k],posl[suml[i-1]+k]) min(posw[sumw[i1]+k],posl[suml[i1]+k]),接下来再考虑“赛点规则”的有影响,通过分析发现,我们找到完成这一局的位置之后,只有此时两人的分差为1是才是赛点。若此时s[i]=s[i+1],那么在i+1出必定结束这一局,否则继续往下进行。所以我们可以从后往前先预处理结束平局的位置B[i],若s[i]=s[i+1],B[i]=i+1。否则 B[i]=B[i+2]。

具体代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1000010,M=998244353;
typedef long long LL;
char s[N];
int sumw[N],suml[N],posw[N],posl[N],B[N];
int n,k1,k2;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    cin>>(s+1);
    for(int i=1;i<=n;i++)
    {
        sumw[i]=sumw[i-1]+(s[i]=='W');
        suml[i]=suml[i-1]+(s[i]=='L');
        if(s[i]=='W')posw[++k1]=i;
        else posl[++k2]=i;
    }
    B[n+1]=n+1;
    B[n]=n+1;
    for(int i=n-1;i>=1;i--)
    {
        if(s[i]==s[i+1])B[i]=i+1;
        else B[i]=B[i+2];
    }
    int ans=0;
    for(int k=1,a=1;k<=n;k++,a=1LL*a*(n+1)%M)
    {
        int cnt=0;
        for(int i=1;i<=n;i++)
        {
            int ni=n+1;
            if(sumw[i-1]+k<=k1)ni=min(ni,posw[sumw[i-1]+k]);//注意不能超过k1,k2.
            if(suml[i-1]+k<=k2)ni=min(ni,posl[suml[i-1]+k]);
            int tw=sumw[ni]-sumw[i-1];
            int tl=suml[ni]-suml[i-1];
            if(abs(tw-tl)==1)ni=B[ni];//分差为1时,直接跳到打破平局的位置
            if(ni>n)break;
            if(ni<=n&sumw[ni]-sumw[i-1]>suml[ni]-suml[i-1])cnt++;
            i=ni;
        }
        ans = (1LL * cnt * a + ans) % M;
    }
    cout<<ans<<"\n";
    return 0;
}
  1. D题 https://ac.nowcoder.com/acm/contest/11256/D dp、最长公共子序列

大意:给定两个字符串 A,B ∣ A ∣ < = 5000 , ∣ B ∣ < = 5000 |A|<=5000,|B|<=5000 A<=5000,B<=5000​ ,即 A的某个子序列为a,B的某个子序列为b,求满足 ∣ a ∣ = ∣ b ∣ |a|=|b| a=b​,且至少存在 一个i满足 a i < b i a_i<b_i ai<bi​​并且对于所有的 j a j = b j ( 1 < = j < = i − 1 ) a_j=b_j (1<=j<=i-1) aj=bj(1<=j<=i1)​​,求所有满足条件的构成方案,多个相同的子序列只要某字母在原序列中的位置不同就算不同的子序列。

思路:先来想想暴力怎么做,因为我们找的是存在一个 k 使得子序列 a k < b k a_k<b_k ak<bk 并且新的子序列 k 之前的部分是相等的,之后的部分可以随便选,保证最终新的子序列的长度相等就好了。所以我们可以先确定 k 的位置,所以我们可以双重循环在原序列中枚举 i,j。 一旦找到

A i < B j A_i<B_j Ai<Bj 之后我们关心的方案是就是前后两部分。对于前面的部分,就是公共子序列,所以我们可以用类似求最长公共子序列的方法来分析问题,只不过我们求的的公共子序列的个数。f[i,j] 表示 A 序列中 前 i 个,B 序列中前 j 个中公共子序列的个数。

根据最后一步将集合划分为 (1)不含 A i A_i Ai​​,不含 B j B_j Bj​​​ (2)含 A i A_i Ai​,不含 B j B_j Bj​ (3)不含 A i A_i Ai​,含 B j B_j Bj​ (4) 含 A i A_i Ai​,含 B j B_j Bj​ 四个部分:

对于第一部分 显然为 f[i-1,j-1]

对于第二部分 直接的用 f[i,j-1] 是不太合适的,而且这里是计数问题,所以会有重复计算的部分,对于f[i,j-1]表示的是 A 中前 i 个,B中前j-1个中对于 B 序列这块是没问题的,但是 A 序列这块是前 i 个,可以含 A i A_i Ai 但是还有不含 A i A_i Ai 的部分,对于第三部分同理,也就是多了不含 B j B_j Bj 的部分 ,也就是说 f[i-1,j-1]被多算了一次,所以要减去。

对于第四部分 当 A i = B j A_i=B_j Ai=Bj 才计算。

到此我们解决了前面的那块,接下来我们来看后面的那块:

对于枚举到了 i , j 对于 A 序列后面可供选择的字母有 ∣ A ∣ − i |A|-i Ai 个,对于B序列 ∣ B ∣ − j |B|-j Bj 个。不妨令 x= ∣ A ∣ − i |A|-i Ai ,y= ∣ B ∣ − j |B|-j Bj ,x<=y

因为后面的字母是随便选,只要保证两个序列选个字母个数相同就行了,所以接下来的问题也就是个组合问题。方案数为:

∑ i = 0 x C ( x , i ) ∗ C ( y , i ) \sum_{i=0}^{x}{C(x,i)} * C(y,i) i=0xC(x,i)C(y,i)

也即是

∑ i = 0 x C ( x , x − i ) ∗ C ( y , i ) \sum_{i=0}^{x}{C(x,x-i)} * C(y,i) i=0xC(x,xi)C(y,i)

从实际意义的角度出发,也就是有两部分球,分别是 x 个,y 个。从第一部分选 x-i 个,从第二部分选 i 个,把 i 所有的情况相加

也就相当于从 x+y 个球中,一次性选 x 个, 也即是 C ( x + y , x ) C(x+y,x) C(x+y,x)​。对于组合数这部分可以提前预处理。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N =5010,M=1e9+7;
typedef long long LL;
LL f[N][N],inv[N*2],fac[N*2],ans;
char a[N],b[N];
int n,m;
int qmi(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1)res=(LL)res*a%M;
		a=(LL)a*a%M;
		b>>=1;
	}
	return res%M;
}
LL C(LL a,LL b)
{
	return fac[a]*inv[b]%M*inv[a-b]%M;
}
void init()
{
	fac[0]=fac[1]=1;
	for(int i=2;i<N*2;i++)fac[i]=fac[i-1]*i%M;
	inv[0]=inv[1]=1;
	for(int i=2;i<N*2;i++)inv[i]=qmi(fac[i],M-2);
}
int main()
{	
	scanf("%s%s",a+1,b+1);
	n=strlen(a+1),m=strlen(b+1);
	init();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			f[i][j]=(f[i-1][j]+f[i][j-1]-f[i-1][j-1]+M)%M;
			if(a[i]==b[j])f[i][j]=(f[i][j]+f[i-1][j-1]+1)%M;
			int x=n-i,y=m-j;
			if(x>y)swap(x,y);
			if(a[i]<b[j])ans=(ans+(f[i-1][j-1]+1)*C(x+y,x))%M; // 注意这里是f[i-1][j-1]+1,y没有公共子序列的时候也是可以的
		}
	printf("%lld\n",ans);
 } 

总结:对问题进行转化、等价变形,抽象成学过的模型。

  1. J 题 https://ac.nowcoder.com/acm/contest/11256/J 二分图最大权匹配

大意:现有 n 个珠宝,(1<=n<=300), 每个珠宝对应一个坐标 ( x i , y i , z i ) (x_i,y_i,z_i) (xi,yi,zi)​ 和速度 v i v_i vi 即z坐标每个时刻增加的值,坐标和速度均为正数,从0时刻开始打捞珠宝,每个时刻只能打捞一个珠宝,代价为该珠宝到远点坐标的平方,求打捞所有珠宝花费的最小代价。

思路:每个时刻只能捞一个珠宝,也就是说珠宝和时刻是一一对应的,求最小代价,也就是求最小的边权和。并且 n<=300 显然就是二分图最大权匹配问题,因为求最小代价和,所以先变成负的,最后在变成正的就好了。

  1. K题 https://ac.nowcoder.com/acm/contest/11256/K 单调队列/ RMQ+st

大意:给定一个长度为 n 的序列,m 次询问,(1<=n<=1e5,1<=m<=200),每次询问给定一个k,求满足区间最大值-最小值>k 的区间个数。

思路:一看区间最值问题,并且是没有修改的,自然而然的应该想到单调队列维护区间最值。如果我们确定了左端点,一旦出现了满足条件的右端点,那么对于之后的右端点也全部都是满足的,这时候左端点继续往后移动并且更新单调队列里的存的定义就好了。这样就可以O(n)的求出满足条件的区间个数,代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=100010;
int a[N],q1[N],q2[N],n,m;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	while(m--)
	{
		int l=1,h1=1,t1=0,h2=1,t2=0,k;
		LL ans=0;
		cin>>k;
		for(int i=1;i<=n;i++)
		{
			while(h1<=t1&&a[q1[t1]]<=a[i])t1--;
			q1[++t1]=i;
			while(h2<=t2&&a[q2[t2]]>=a[i])t2--;
			q2[++t2]=i;
			while(h1<=t1&&h2<=t2&&a[q1[h1]]-a[q2[h2]]>k)
			{
				ans+=n-i+1;
				l++;
				while(q1[h1]<l)h1++;
				while(q2[h2]<l)h2++;
			}
		}
		cout<<ans<<"\n";
	}
	return 0;
}

另外也可以用st表+双指针做。随着区间的增加最大值不会变小,最小值不会变大。我们枚举区间的时候,对于某段[l,r] 满足条件的话,对于之后的 r 也是满足的,这之后 l++,r是不会往前找的, 因为区间最小值变的更小,最大值变的更大,所以 我们固定 l 找最小的 r 时,r是有递增性的,我们预处理出区间最值后就可以O(1)的查询,双指针O(n)的复杂度更新答案。代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=100010,M=17;
int a[N],f1[N][M],f2[N][M],n,m,st[N];
void init()
{
    for(int j=0;j<M;j++)
    {
        int t=1<<j;
        if(t>=N)break;
        st[t]=j;
    }
    for(int i=1;i<N;i++)if(!st[i])st[i]=st[i-1];
    for(int j=0;(1<<j)<=n;j++)
        for(int i=1;i+(1<<j)-1<=n;i++)
            if(!j)f1[i][j]=f2[i][j]=a[i];
            else 
            {
                f1[i][j]=max(f1[i][j-1],f1[i+(1<<j-1)][j-1]);
                f2[i][j]=min(f2[i][j-1],f2[i+(1<<j-1)][j-1]);
            }
}
int q1(int l,int r)
{
    int k=r-l+1;
    k=st[k];
    return max(f1[l][k],f1[r-(1<<k)+1][k]);
}
int q2(int l,int r)
{
    int k=r-l+1;
    k=st[k];
    return min(f2[l][k],f2[r-(1<<k)+1][k]);
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
    init();
	while(m--)
	{
		LL ans=0;
        int k;
        cin>>k;
        for(int i=1,j=1;i<=n;i++)
        {
            while(i<=j&&j<=n&&q1(i,j)-q2(i,j)<=k)j++;
            if(j<=n)ans+=n-j+1;
        }
        cout<<ans<<"\n";
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值