2023.8.7测试

\[\text{暑假NOIP模拟赛-2 保龄记} \]

当然也搬了很多原题

T1 转圈圈

一个长度为 \(n\)\(01\) 串,初始时只有 \(s\) 位置上是 \(1\)。一次操作可以使一个长度为 \(k\) 的字串翻转。对于每个位置 \(i\),你要求出最少需要几次操作使得这个位置为 \(1\),否则输出 \(-1\)

同时,串中存在 \(m\) 个禁止位置,这些位置永远都不能为 \(1\)

\(1\leq n\leq 10^5\)

为了卡常最后时刻手写队列直接写成栈,喜提 \(0\)

手玩可注意到一个点可以往一段区间内奇偶性相同的点连边权为 \(1\) 的边,直接连边再 \(\rm bfs\) 复杂度 \(O(n^2)\)。于是选择线段树优化建图,时间复杂度 \(O(n\log n+\log n)\)

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

const int N=100010,M=1000010;

int n,m,k,s,a[N],f[M],id[N],cnt; 
bool v[N],vis[M];
vector < pair<int,int> > g[M];
deque <int> q;

struct SegmentTree
{
	int idd[4*N],pos[N];
	
	void build(int p,int l,int r)
	{
		if(l==r)
		{
			idd[p]=pos[l]=++cnt;
			return;
		}
		
		idd[p]=++cnt;
		int mid=(l+r)>>1;
		build(p*2,l,mid);  g[idd[p]].push_back(mp(idd[p*2],0));
		build(p*2+1,mid+1,r);  g[idd[p]].push_back(mp(idd[p*2+1],0));
	}
	
	void add(int p,int l,int r,int ql,int qr,int x)
	{
		if(ql<=l && qr>=r)
		{
			g[x].push_back(mp(idd[p],1));
			return;
		}
		
		int mid=(l+r)>>1;
		if(ql<=mid)
			add(p*2,l,mid,ql,qr,x);
		if(qr>mid)
			add(p*2+1,mid+1,r,ql,qr,x);
	}
}tree[2];

void bfs()  //重点注意01bfs的写法
{
	f[id[s]]=0;  q.push_front(id[s]);
	while(q.size())
	{
		int x=q.front();  q.pop_front();
		if(vis[x])	
			continue;
		vis[x]=1;
		for(int i=0; i<g[x].size(); i++)
		{
			int y=g[x][i].first,z=g[x][i].second;
			if(f[y]==-1 || f[x]+z<f[y])
			{
				f[y]=f[x]+z;
				if(z==0)
					q.push_front(y);
				else
					q.push_back(y);
			}
		}
	}
}

int main()
{
	memset(f,-1,sizeof(f));
	
	scanf("%d%d%d%d",&n,&k,&m,&s);
	for(int i=1; i<=m; i++)
	{
		int x;
		scanf("%d",&x);
		v[x]=1;
	}
  
	tree[0].build(1,1,n);  tree[1].build(1,1,n);
	for(int i=1; i<=n; i++)
	{
		id[i]=tree[i&1].pos[i];
		if(v[i])
			continue;
		int l=(i>=k)? i-(k-1):k-i+1;
		int r=(i+k-1<=n)? i+(k-1):n-k+1+n-i;
		tree[(i&1)^((k&1)^1)].add(1,1,n,l,r,id[i]);	
	}
	
	bfs();
	
	for(int i=1; i<=n; i++)
		printf("%d ",v[i]? -1:f[id[i]]);
	
	return 0;
}

T2 括号匹配

(Uoj原题)

打部分分开了 \(\rm deque\) 直接 \(\rm MLE\) 喜提 \(0\)

注意到一个区间 \([i,j]\) 合法当且仅当满足以下条件:

  • \(j-i+1\) 是偶数

  • \()\) 当作 \(-1\),其余当作 \(1\),区间的所有前缀和都 \(\geq 0\),且总和等于 \(0\)

总和等于 \(0\) 很难处理,于是考虑转化成如下条件:

  • \()\) 当作 \(-1\),其余当作 \(1\),区间的所有前缀和都 \(\geq 0\)

  • \((\) 当作 \(-1\),其余当作 \(1\),区间的所有后缀和都 \(\geq 0\)

根据这些条件预处理出两个数组 \(L[i],R[i]\),分别表示以 \(i\) 为起点,向左/向右最远能到哪里,可以单调栈 \(O(n)\) 求出

那么现在再来考虑区间 \([i,j]\) 的合法性,我们可以写成如下的式子:

  • \(j-i+1\equiv 0 \pmod 2\)

  • \(i\geq L[j]\)

  • \(j\leq R[i]\)

转化一下变成在平面直角坐标系中,有多少个点 \((L[j],j)\) 在点 \((i,R[i])\) 的左下方,这是一个二维数点的问题,可以用树状数组解决

但是数点要满足 \(j>i\),这样很难操作。注意到当 \(j<i\)\(L[j]\leq j<i\),此时必定满足条件,于是我们可以先全部加到答案里在减去比 \(i\) 小的 \(j\) 的贡献

时间复杂度 \(O(n\log n)\)

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

const int N=1000010;

int n,s1[N],s2[N],L[N],R[N];
int sta[N],top,cnt0,cnt1;
struct node{int x,y;}tmp0[N],tmp1[N];
char s[N];
LL ans;

bool cmp(node a,node b)
{
	return a.x<b.x;
}

struct BIT
{
	int c[N];
	
	void add(int x,int y)
	{
		x++;
		for(x; x<=n+1; x+=(x&-x))
			c[x]+=y;
	}
	
	int ask(int x)
	{
		x++;
		int res=0;
		for(x; x; x-=(x&-x))
			res+=c[x];
		return res;
	}
}t[2];

void prework()
{
	s1[n+1]=-n;  sta[++top]=0;
	for(int i=1; i<=n+1; i++)
	{
		while(top && s1[i]<s1[sta[top]])
			R[sta[top]+1]=i-1,top--;
		sta[++top]=i;
	}
	
	top=0;  s2[0]=-n;  sta[++top]=n+1;
	for(int i=n; i>=0; i--)
	{
		while(top && s2[i]<s2[sta[top]])
			L[sta[top]-1]=i+1,top--;
		sta[++top]=i;
	} 
	
	for(int i=0; i<=n+1; i++)
	{
		if(s[i]=='(')
			L[i]=i;
		else if(s[i]==')')
			R[i]=i;
	}	
}

int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	
	for(int i=1; i<=n; i++)
		s1[i]=s1[i-1]+(s[i]==')'? -1:1);
	for(int i=n; i>=1; i--)
		s2[i]=s2[i+1]+(s[i]=='('? -1:1);
		
	prework(); 
	
	for(int i=2; i<=n; i+=2)
		tmp0[++cnt0]=(node){L[i],i};
	for(int i=1; i<=n; i+=2)
		tmp1[++cnt1]=(node){L[i],i};
		
	sort(tmp0+1,tmp0+1+cnt0,cmp);
	sort(tmp1+1,tmp1+1+cnt1,cmp);
	
	int p0=1,p1=1;
	for(int i=1; i<=n; i++)
	{
		if(i&1)
		{
			while(p0<=cnt0 && tmp0[p0].x<=i)
				t[0].add(tmp0[p0].y,1),p0++;
			ans+=1LL*(t[0].ask(R[i])-(i-1)/2);
		}
		else
		{
			while(p1<=cnt1 && tmp1[p1].x<=i)
				t[1].add(tmp1[p1].y,1),p1++;
			ans+=1LL*(t[1].ask(R[i])-i/2);
		}
	}
	
	printf("%lld",ans);

	return 0;
}

T3 崩原之战 1

其实就是 P8908 [USACO22DEC] Palindromes P

很有意思的题目

首先考虑暴力,将原串看成 \(01\) 串,显然对于区间 \([l,r]\),如果有奇数个 \(0\) 和奇数个 \(1\),那它的贡献就是 \(-1\)

否则,只要其中一种字符两两配对,那另一种字符肯定也配对了。所以我们考虑少的那一种字符(节省时间),假设是 \(1\)。那显然,交换相邻相同的字符肯定不优。所以我们肯定是首尾配对,如果还剩下一个的话就放在正中间。然后手玩可以发现肯定存在某一种最优的方案,使得配对的两个字符有一个不移动。假设区间 \([l,r]\) 里有 \(m\)\(1\),位置分别是 \(a_{1\sim m}\),那我们可以写出最小的操作次数:

\[[m\equiv 1\pmod 2]\left|\dfrac{l+r}{2}-a_{\left\lfloor m/2\right\rfloor+1}\right|+\sum_{i=1}^{\left\lfloor m/2\right\rfloor}{\left|a_i+a_{m-i+1}-l-r\right|} \]

这样时间复杂度 \(O(n^3)\)

前面的那坨可以 \(O(n^2)\) 枚举解决掉,但后面那坨带绝对值的求和非常恶心,思考如何转化

注意到我们暴力枚举区间时会改变字符的配对情况,那如果反过来固定字符的配对情况再去枚举区间呢?

我们可以先枚举中间的一个或两个字符,每次再往外扩展一层,比如现在扩展到 \(a_i\)\(a_j\) 的配对,那我们就去枚举区间 \(l\in (a_{i-1},a_i],\: r\in[a_j,a_{j+1})\),这样再中心点确定的情况下字符的配对情况也是确定的。

那如何快速计算字符配对对区间的贡献呢?

\(p=a_i+a_j\),集合 \(P\) 为目前已经匹配的字符的所有 \(p\) 构成的集合。将那个绝对值式子分类讨论,分成 \(a_i+a_j\leq l+r\)\(a_i+a_j>l+r\)。对于前者,需要快速查询 \(P\) 内比 \(l+r\) 小的元素的个数和总和,分别记为 \(t\)\(s\)。那么贡献就是 \(t(l+r)-s\)。对于后者,我们记 \(sum=\sum\limits_{p_i\in P}{p_i}\),那么贡献就是 \(sum-s-(|P|-t)(l+r)\)

想要快速查询集合内比 \(x\) 小的元素总和和个数?\(\rm BIT\)!!!

为什么这样枚举可以呢?因为在中心点不同的情况下,每个区间 \([l,r]\) 只会被枚举一次,所以时间复杂度 \(O(n^2\log n)\)

于是,这道题就愉快地结束了

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

const int N=7510;

int n,a[N],cnt;
LL ans;
char s[N];

struct BIT
{
	LL c[2*N];
	
	void add(int x,int y)
	{
		for(x; x<=2*n; x+=(x&-x))
			c[x]+=(LL)y;
	}
	
	LL ask(int x)
	{
		LL res=0;
		for(x; x; x-=(x&-x))
			res+=c[x]; 
		return res;
	}
	
	void clear()
	{
		for(int i=1; i<=2*n; i++)
			c[i]=0;
	}
}t1,t2;

int main()
{
	scanf("%s",s+1);  n=strlen(s+1);
	
	int cntg=0; 
	for(int i=1; i<=n; i++)
		cntg+=(s[i]=='G');
	if(cntg>n/2)  //将出现次数少的字符作为操作的对象
		for(int i=1; i<=n; i++)
			s[i]=(s[i]=='G'? 'H':'G');
	
	for(int l=1; l<=n; l++)
	{
		cnt=0;
		for(int r=l; r<=n; r++)
		{
			if(s[r]=='G')
				a[++cnt]=r;
			if((cnt&1) && (r-l+1)%2==0)  //0和1都出现奇数次,无解,贡献-1
				ans--;
			else if(cnt&1)  //提前计算式子的前半部分
				ans+=abs((l+r)/2-a[cnt/2+1]);
		}
	}
	
	cnt=0;
	for(int i=1; i<=n; i++)
		if(s[i]=='G')
			a[++cnt]=i;
	a[cnt+1]=n+1;
	
	for(int i=1; i<=cnt; i++)  //枚举一个中间的一个字符
	{
		t1.clear();  t2.clear();
		LL sum=0;
		
		for(int j=1; i-j>=1 && i+j<=cnt; j++)
		{
			int tmp=a[i-j]+a[i+j];
			sum+=(LL)tmp;
			t1.add(tmp,1);  t2.add(tmp,tmp);
			
			for(int l=a[i-j-1]+1; l<=a[i-j]; l++)
			{
				for(int r=a[i+j]; r<=a[i+j+1]-1; r++)
				{
					if((r-l+1)%2==0)
						continue;
					LL t=t1.ask(l+r),s=t2.ask(l+r);
					ans+=1LL*(t*(l+r)-s+(sum-s)-(j-t)*(l+r));
				}
			}
		}
	} 
	
	for(int i=1; i<cnt; i++)  //枚举中间的两个字符
	{
		t1.clear();  t2.clear();
		LL sum=0;
		
		for(int j=0; i-j>=1 && i+j+1<=cnt; j++)
		{
			int tmp=a[i-j]+a[i+j+1];
			sum+=(LL)tmp;
			t1.add(tmp,1);  t2.add(tmp,tmp);
			
			for(int l=a[i-j-1]+1; l<=a[i-j]; l++)
			{
				for(int r=a[i+j+1]; r<=a[i+j+2]-1; r++)
				{
					LL t=t1.ask(l+r),s=t2.ask(l+r);
					ans+=1LL*(t*(l+r)-s+(sum-s)-(j+1-t)*(l+r));
				}
			}
		}
	}
	
	printf("%lld",ans);

	return 0;
}

T4 抽卡 1

其实就是P9379 [THUPC 2023 决赛] 老虎坤

最后时刻想冲部分分没冲出来(是自己想简单了)。做完这题后对期望有了更深入的认识

copy一下别人的题解

设一个位置集合 \(A\) 表示每一位的取值情况,\(A\) 的每一位是 \(\{0,1,?\}\) 的一种,所以 \(A\) 可以用三进制表示。称 \(A\) 合法,当且仅当 \(A\) 可以唯一确定目标串

对于操作次数的期望,一个经典套路是计算到达某个合法状态的概率,乘上停留在这里的期望时间,再求和。(注意状态每一位是 \(\{0,1\}\),表示是否知道该位的数字,是二进制)

考虑预处理停留在状态 \(S\) 的期望时间 \(t_S\)。设 \(P'_S\) 表示停留在 \(S\) 的概率,那么 \(P'_S=\prod\limits_{i\not\in S}(1-p_i)\)。根据经典结论 \(t_S=\prod\limits_{i=0}^{\infty}(P'_S)^i=\dfrac{1}{1-P'_S}\)(其实我是第一次知道,考完后Shui_Dream跟我分析了下才明白)

那么从 \(P_S\) 转移到 \(P_T\) 的系数就是 \(\prod\limits_{i\not\in S,i\in T}{p_i}\prod\limits_{i\not\in T}{(1-p_i)}t_S=\dfrac{\prod\limits_{i\not\in S,i\in T}{p_i}\prod\limits_{i\not\in T}{(1-p_i)}}{1-P'_S}\),预处理 \(g_{1,S}=\prod\limits_{i\in S}{p_i}\)\(g_{2,S}=\prod\limits_{i\\not\in S}{(1-p_i)}\) 可以快速求出。枚举超集 \(\rm DP\) 时间复杂度 \(O(3^l)\)

对于目标串 \(s_i\),设 \(Q_{i}\) 表示 \(s_i\) 的合法位置集合,那么答案就等于 \(\sum\limits_{A\not\in Q_{i}}{P_At_A}\)。发现对于一个 \(A\),它要么唯一确定一个字符串,要么不能,而 \(A\) 的每一位都是 \(\{0,1,?\}\) 中的一个,因此 \(A\) 的个数是 \(O(3^l)\) 的,因此 \(\sum{|Q_i|}\leq 3^l\)。于是我们考虑容斥,\(ans=\sum\limits_{A}{P_At_A}-\sum\limits_{A\in Q_i}{P_At_A}\)

现在问题在于如何快速求出一个字符串 \(s\) 的合法位置集合。设 \(pt_A\) 表示 \(A\) 能唯一确定的字符串编号,如果不唯一则为 \(-1\),谁都无法确定就为 \(0\)

先考虑暴力,对于任意的 \(A\),选一位 \(0\) 或者 \(1\) 把它变成 \(?\),看它是否能确定其他的字符串,这样复杂度是 \(O(3^ll)\)

但如果反过来,我们选一些 \(?\),将它填上 \(0\) 或者 \(1\),看看两次的结果是否不同。若相同或有一个是 \(0\),就可以转移:令 \(pt_A\) 等于 \(pt_A'\),否则就为 \(-1\)。稍微思考一下就能发现我们只要填一个 \(?\) 就可以了,于是我们可以预处理出每个 \(A\) 最靠左的 \(?\),这样时间复杂度就是 \(O(3^l)\)

那么总的时间复杂度就是 \(O(T3^l)\)

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

const int M=20,N=40010,MAXN=15000010;
const int MOD=998244353;

int T,n,l,c[M],a[N],ans[N];
int p,pw[M],mi[MAXN],val[MAXN],pt[MAXN];  //mi表示最左边的?,val表示?位置的集合 
int g1[N],g2[N],f[MAXN];

void init()
{
	memset(f,0,sizeof(f));
	memset(pt,0,sizeof(pt));
	memset(val,0,sizeof(val));
}

int ksm(int x,int y)
{
	int res=1;
	while(y)
	{
		if(y&1)
			res=1LL*res*x%MOD;
		x=1LL*x*x%MOD;
		y>>=1;
	}
	return res;
}

void prework()
{
	pw[0]=1;
	for(int i=1; i<=15; i++)
		pw[i]=pw[i-1]*3;
		
	mi[0]=0;
	for(int i=0; i<15; i++)  
	{
		for(int j=pw[i]-1; j>=0; j--)  //预处理每个位置集合最左边的? 
		{
			mi[j*3+0]=mi[j]+1;
			mi[j*3+1]=mi[j]+1;
			mi[j*3+2]=0;
		}
	}
}

void calc()  //计算每个位置集合是否能唯一确定一个字符串 
{
	for(int i=0; i<=pw[l]-1; i++) 
	{
		if(mi[i]>=l)  //大于等于l说明没有? 
			continue;
		int pre=i-pw[mi[i]];  //?位置填1
		val[i]=val[pre]^(1<<mi[i]); 
		pt[i]=pt[pre];
		pre=i-pw[mi[i]]*2;  //?位置填0 
		if(!pt[i])
			pt[i]=pt[pre];
		else if(pt[i]!=pt[pre] && pt[pre]!=0)
			pt[i]=-1;
	}
}

int main()
{
	p=ksm(10000,MOD-2); 
	prework();
	
	scanf("%d",&T);
	while(T--)
	{
		init();
		
		scanf("%d%d",&l,&n);
		for(int i=0; i<l; i++)
			scanf("%d",&c[i]);
		for(int i=1; i<=n; i++)
		{
			char x[N];
			scanf("%s",x);
			a[i]=0;
			for(int j=0; j<l; j++)
				if(x[j]=='1')
					a[i]+=pw[j];
			pt[a[i]]=i;
		} 
			
		if(n==0)
		{
			printf("0\n");
			return 0; 
		}
		
		calc();
		
		for(int i=0; i<=(1<<l)-1; i++)  //预处理g1,g2 
		{
			g1[i]=g2[i]=1;
			for(int j=0; j<l; j++)
			{
				if(i&(1<<j))
				{
					g1[i]=1LL*g1[i]*c[j]%MOD*p%MOD;
					g2[i]=1LL*g2[i]*(10000-c[j])%MOD*p%MOD;
				}
			}
		}
		
		f[0]=1;
		int sum=0,S=(1<<l)-1;
		for(int i=0; i<=S; i++)
		{
			f[i]=1LL*f[i]*ksm((1-g2[S^i]+MOD)%MOD,MOD-2)%MOD;  //先乘上期望时间 
			for(int j=i; j<=S; j=(j+1)|i)  //枚举超集 
			{
				if(i==j)
					continue;
				f[j]=(f[j]+1LL*f[i]*g1[j^i]%MOD*g2[S^j]%MOD)%MOD;
			}
			sum=1LL*(sum+f[i])%MOD;
		}
		
		for(int i=1; i<=n; i++)
			ans[i]=sum;
		for(int i=0; i<=pw[l]-1; i++)  //容斥 
			if(pt[i]>0)
				ans[pt[i]]=(ans[pt[i]]-f[S^val[i]]+MOD)%MOD;
		
		for(int i=1; i<=n; i++)
			printf("%d\n",ans[i]);
	}

	return 0;
}
posted @ 2023-08-08 16:13  xishanmeigao  阅读(36)  评论(0)    收藏  举报