20240706比赛总结

题外话:IOI赛制的一大好处是可以猜解法,密码已改,不要试图jc我

T1 公式求值

https://gxyzoj.com/d/hzoj/p/3735

根据样例解释,显然在不进位的情况下,倒数第一位是所有位上数字的总和,倒数第二位是所有位上数字的总和减去最后一位的数字

以此类推,显然前缀和,在处理一下进位即可

代码:

#include<cstdio>
#include<string>
#include<iostream>
using namespace std;
int n,ans[1000005],sum[500005],cnt;
string s;
int main()
{
	cin>>s;
	n=s.size();
	s=" "+s;
	for(int i=1;i<=n;i++)
	{
		sum[i]=s[i]-'0';
		sum[i]+=sum[i-1];
	}
	for(int i=1,j=n;i<=n;i++,j--)
	{
		ans[i]=sum[j];
	}
	for(int i=1;i<=n;i++)
	{
		ans[i+1]+=ans[i]/10;
		ans[i]=ans[i]%10;
	}
	cnt=n;
	while(ans[cnt+1])
	{
		cnt++;
		ans[cnt+1]=ans[cnt]/10;
		ans[cnt]%=10;
	}
	for(int i=cnt;i>0;i--)
	{
		printf("%d",ans[i]);
	}
	return 0;
}

T2 最长的Y

https://gxyzoj.com/d/hzoj/p/3736

当Y的个数和位置确定时,根据绝对值的几何意义,显然当终点在中间时是操作次数最小

此处可以用前缀和然后枚举中间点

然后考虑长度,因为当x可以完成时,x-1显然可以,所以直接二分即可

代码:

#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
int n,p[200005],cnt;
ll k,sum[200005];
string s;
bool check(int x)
{
	int mid=(x+1)>>1;
	int l=mid-1,r=x-mid;
	ll t1=l*(l+1)/2,t2=r*(r+1)/2,ans=1e18+1;
	for(int i=mid;i<=cnt-r;i++)
	{
		ll l_step=p[i]*l-sum[i-1]+sum[i-mid]-t1;
		ll r_step=sum[i+r]-sum[i]-p[i]*r-t2;
	//	printf("%d %d %d\n",i,l_step,r_step);
		ans=min(ans,l_step+r_step);
	}
	if(ans<=k) return 1;
	return 0;
}
int main()
{
	cin>>s;
	scanf("%lld",&k);
	n=s.size();
	for(int i=1;i<=n;i++)
	{
		if(s[i-1]=='Y') p[++cnt]=i;
	}
	for(int i=1;i<=cnt;i++)
	{
		sum[i]=sum[i-1]+p[i];
	}
	int l=1,r=cnt;
	while(l<r)
	{
		int mid=(l+r+1)>>1;
	//	printf("%d\n",mid);
		if(check(mid))
		{
			l=mid;
		}
		else r=mid-1;
	}
	printf("%d",l);
	return 0;
}

T3 交换序列

https://gxyzoj.com/d/hzoj/p/3737

这里有三种元素,且串的长度很小,操作次数也不大

所以直接设\(dp_{i,j,k,t}\)表示目前E排好了i个,K排好了j个,Y排好了k个,且操作了t次

i,j,k可以直接枚举,考虑t的增量

因为相同的字母之间不用互换,所以出去当前变换的字母之外,其他字母的相对位置不变

如果此时要放一个E,假设在这个E的前面有t1个K,t2个Y,如果\(t1<j\),则它前面还有\(j-t1\)个K需要和它交换,否则就不需要和K交换

和Y就同理,注意要分别计算,且分别对0取max再相加

代码:

#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
int n,K,id1[35],id2[35],id3[35],t1,t2,t3;
int sum[4][35];
string s;
ll dp[32][32][32][501];
void solve(int i,int j,int k)
{
	int tmp1=max(sum[2][id1[i+1]]-j,0)+max(sum[3][id1[i+1]]-k,0);
	int tmp2=max(sum[1][id2[j+1]]-i,0)+max(sum[3][id2[j+1]]-k,0);
	int tmp3=max(sum[2][id3[k+1]]-j,0)+max(sum[1][id3[k+1]]-i,0);
//	printf("%d %d %d %d %d %d\n",i,j,k,tmp1,tmp2,tmp3);
	for(int t=0;t<=K;t++)
	{
		if(t+tmp1<=K) dp[i+1][j][k][t+tmp1]+=dp[i][j][k][t];
		if(t+tmp2<=K) dp[i][j+1][k][t+tmp2]+=dp[i][j][k][t];
		if(t+tmp3<=K) dp[i][j][k+1][t+tmp3]+=dp[i][j][k][t];
	}
}
int main()
{
	cin>>s;
	n=s.size();
	s=" "+s;
	scanf("%d",&K);
	for(int i=1;i<=n;i++)
	{
		if(s[i]=='E') id1[++t1]=i,sum[1][i]++;
		if(s[i]=='K') id2[++t2]=i,sum[2][i]++;
		if(s[i]=='Y') id3[++t3]=i,sum[3][i]++;
	}
	for(int i=1;i<=n;i++)
	{
		sum[1][i]+=sum[1][i-1];
		sum[2][i]+=sum[2][i-1];
		sum[3][i]+=sum[3][i-1];
	}
	dp[0][0][0][0]=1;
	for(int i=0;i<=t1;i++)
	{
		for(int j=0;j<=t2;j++)
		{
			for(int k=0;k<=t3;k++)
			{
				solve(i,j,k);
			}
		}
	}
	ll ans=0;
	for(int i=0;i<=K;i++)
	{
		ans+=dp[t1][t2][t3][i];
	}
	printf("%lld",ans);
	return 0;
}

T4 最长路径

https://gxyzoj.com/d/hzoj/p/3738

\(dp_{i,j,k}\)表示目前在位置\((i,j)\),取了k个值的最小值

关键在于如何找那些值要,那些值不要

可以暴力枚举临界值,将大于等于的都选上,此时,可以得到68分,因为与临界值相等的可选可不选

那么如何处理这种情况呢?

可以再用一个新的值区分相同的值,这样两个值就可以控制相等的数的数量了

代码:

#include<cstdio>
#include<algorithm>
#include<map>
#define ll long long
using namespace std;
int n,m,K,a[35][35],d[35][35];
ll dp[35][35][65],ans=1e17;
map<int,int>mp;
void solve(int x,int y)
{
	for(int i=0;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			for(int k=0;k<=K;k++)
			{
				dp[i][j][k]=1e17;
			}
		}
	}
	dp[1][0][0]=dp[0][1][0]=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(a[i][j]>x||(a[i][j]==x&&d[i][j]>=y))
			{
				for(int k=1;k<=K;k++)
				{
					dp[i][j][k]=min(dp[i-1][j][k-1],dp[i][j-1][k-1])+a[i][j];
				}
			}
			else
			{
				for(int k=0;k<=K;k++)
				{
					dp[i][j][k]=min(dp[i-1][j][k],dp[i][j-1][k]);
				}
			}
		}
	}
	ans=min(ans,dp[n][m][K]);
}
int b[65],c[65];
ll get_ans()
{
	int cnt=1,x=1,y=1;
	c[1]=a[1][1];
	for(int i=1;i<=n+m-2;i++)
	{
		if(b[i]==1) x++;
		else y++;
		c[++cnt]=a[x][y];
	}
	sort(c+1,c+cnt+1);
	ll sum=0;
	for(int i=cnt;i>cnt-K;i--)
	{
		sum+=c[i];
	}
	return sum;
}
void dfs(int x,int y)
{
	if(x>n+m-2)
	{
		if(y!=0) return;
		ans=min(ans,get_ans());
		return;
	}
	if(y)
	{
		b[x]=1;
		dfs(x+1,y-1);
		b[x]=0;
	}
	b[x]=-1;
	dfs(x+1,y);
}
int main()
{
	scanf("%d%d%d",&n,&m,&K);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&a[i][j]);
			d[i][j]=++mp[a[i][j]];
		}
	}
	if(n<=10&&m<=10)
	{
		dfs(1,n-1);
		printf("%lld",ans);
		return 0;
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			solve(a[i][j],d[i][j]);
		}
	}
	printf("%lld",ans);
	return 0;
}

补充题目

[ABC217F] Make Pair

https://gxyzoj.com/d/hzoj/p/3309

\(dp_{i,j}\)表示i到j这个区间有多少种取法

当i和i+1就是一对时可以直接取走,\(dp_{i,i+1}=1\),这是初始状态

接下来考虑转移,首先,如果在取完中间后,i,j是一对可以直接取出的,就直接转移,即\(dp_{i,j}+=dp_{i+1,j-1}\)

接下来考虑将原串分为两部分,即\(A/xBy\)的形式,A,B均为可以经过一定次数消除的串,而x,y是两个人且可以直接消除

为什么要加x,y? 因为如果直接分开会出现重复,这样可以去重

所以式子就是\(dp_{i,j}+=dp_{i,k-1}\times dp_{k+1,j}\times C_{\frac{len}{2}}^{\frac{j-k+1}{2}}\)

点击查看代码
#include<cstdio>
#define ll long long
using namespace std;
int n,m,mod=998244353;
bool fl[405][405];
ll dp[405][405],fac[405],inv[405];
ll qpow(ll x,int y)
{
	ll res=1;
	while(y)
	{
		if(y&1) res=res*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return res;
}
ll C(int x,int y)
{
	x/=2,y/=2;
	return fac[y]*inv[x]%mod*inv[y-x]%mod;
}
int main()
{
	scanf("%d%d",&n,&m);
	n*=2;
	fac[0]=1;
	for(int i=1;i<=n;i++)
	{
		fac[i]=fac[i-1]*i%mod;
	}
	inv[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i>=0;i--)
	{
		inv[i]=inv[i+1]*(i+1)%mod;
	}
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		fl[x][y]=fl[y][x]=dp[x][y]=dp[y][x]=1;
	}
	for(int len=4;len<=n;len+=2)
	{
		for(int i=1;i+len-1<=n;i++)
		{
			int j=i+len-1;
			if(fl[i][j])
			{
				dp[i][j]=dp[i+1][j-1];
			}
			for(int k=i+2;k<j;k+=2)
			{
				if(fl[k][j])
				dp[i][j]=(dp[i][j]+dp[i][k-1]*dp[k+1][j-1]%mod*C(j-k+1,len)%mod)%mod;
			}
		}
	}
	printf("%lld",dp[1][n]);
	return 0;
}

[USACO17JAN] Subsequence Reversal P

https://gxyzoj.com/d/hzoj/p/1

区间dp,和T4很像,如果知道一个区间的最长不下降子序列的最大最小值,就可以很快的判断一个值能否加入

所以设\(dp_{i,j,k,l}\)表示区间i到j,最小值不小于k,最大值不大于l的最长不下降子序列

为方便统计,先转移最大最小值就是k和l的

此时,假如不翻转,则可以从左或从右转移,即:

dp[i][j][k][l]=max(dp[i][j][k][l],max(dp[i][j-1][k][l]+(a[j]==l),dp[i+1][j][k][l]+(a[i]==k)));

假如翻转,则从中间转移,即:

dp[i][j][k][l]=max(dp[i][j][k][l],dp[i+1][j-1][k][l]+(a[j]==k)+(a[i]==l));

注意,因为翻转后左右两边不一定都相等,所以要分开判断

最后记得将答案扩展为最小值不小于k,最大值不大于l的最长不下降子序列

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
int n,a[55],dp[51][51][51][51],mx;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		mx=max(mx,a[i]);
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=a[i];j++)
		{
			for(int k=a[i];k<=mx;k++)
			{
				dp[i][i][j][k]=1;
			}
		}
	}
//	for(int i=1;i<n;i++)
//	{
//		int t1=min(a[i],a[i+1]),t2=max(a[i],a[i+1]);
//		for(int j=1;j<=t2;j++)
//		{
//			for(int k=max(j,t1);k<=mx;k++)
//			{
//				if(j>t1&&k<t2) continue;
//				if(t1<j||k<t2) dp[i][i+1][j][k]=1;
//				else dp[i][i+1][j][k]=2;
//			}
//		}
//	}
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i+len-1<=n;i++)
		{
			int j=i+len-1;
			int t1=min(a[i],a[j]),t2=max(a[i],a[j]);
			for(int len1=1;len1<=mx;len1++)
			{
				for(int k=1;k+len1<=mx;k++)
				{
					int l=k+len1;
					dp[i][j][k][l]=max(dp[i][j][k+1][l],dp[i][j][k][l-1]);
					dp[i][j][k][l]=max(dp[i][j][k][l],dp[i+1][j-1][k][l]+(a[j]==k)+(a[i]==l));
					dp[i][j][k][l]=max(dp[i][j][k][l],max(dp[i][j-1][k][l]+(a[j]==l),dp[i+1][j][k][l]+(a[i]==k)));
				}
			}
		}
	}
//	for(int i=1;i<=n;i++)
//	{
//		for(int j=i;j<=n;j++)
//		{
//			for(int k=1;k<=mx;k++)
//			{
//				for(int l=k;l<=mx;l++)
//				{
//					printf("%d %d %d %d %d\n",i,j,k,l,dp[i][j][k][l]);
//				}
//			}
//		}
//	}
	printf("%d",dp[1][n][1][mx]);
	return 0;
}

括号序列再战猪猪侠

https://gxyzoj.com/d/hzoj/p/P7

\(dp_{i,j}\)为第i组括号到第j组括号的合法排列

分为三种情况:

(A):判断如果第i个左括号对应的右括号的位置比其他左括号对应的位置靠后或没有限制,就可以转移,即\(dp_{i,j}+=dp_{i+1,j}\)

()A:就是在前面加一组括号,如果第i个左括号对应的右括号的位置比其他左括号对应的位置靠前或没有限制,就可以转移,即\(dp_{i,j}+=dp_{i+1,j}\)

(A)B:前面括号的意义在于去重,这里可以枚举断点,要满足这些条件才可以转移

  1. 前面k个左括号对应的右括号位置小于后面左括号对应的右括号位置

  2. 第i个左括号对应的右括号位置小于第i+1到k个左括号对应的位置

此时,转移方程为:\(dp_{i,j}+=dp_{i+1,k}\times dp_{k+1,j}\)

而对于大小关系的判断,用二维前缀和即可

点击查看代码
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
int T,n,m,mod=998244353;
int sum[305][305];
ll dp[305][305];
void init()
{
	memset(sum,0,sizeof(sum));
	memset(dp,0,sizeof(dp));
}
int get_sum(int a,int b,int c,int d)
{
	return sum[c][d]-sum[c][b-1]-sum[a-1][d]+sum[a-1][b-1];
}
void solve()
{
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i+len-1<=n;i++)
		{
			int j=i+len-1;
			int tmp=get_sum(i,i+1,i,j);
			if(!tmp) dp[i][j]=dp[i+1][j];
			tmp=get_sum(i+1,i,j,i);
			if(!tmp) dp[i][j]=(dp[i][j]+dp[i+1][j])%mod;
			for(int k=i+1;k<j;k++)
			{
				tmp=get_sum(k+1,i,j,k);
				int tmp2=get_sum(i,i+1,i,k);
				if(!tmp&&!tmp2)
				dp[i][j]=(dp[i][j]+dp[i+1][k]*dp[k+1][j]%mod)%mod;
			}
		}
	}
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		init();
		scanf("%d%d",&n,&m);
		for(int i=1;i<=m;i++)
		{
			int x,y;
			scanf("%d%d",&x,&y);
			sum[x][y]=1;
		}
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
				sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
			}
		}
		int fl=0;
		for(int i=1;i<=n;i++)
		{
			if(get_sum(i,i,i,i))
			{
				printf("0\n");
				fl=1;
				break;
			}
		}
		if(fl) continue;
		for(int i=1;i<=n;i++) dp[i][i]=1;
		solve();
		printf("%lld\n",dp[1][n]%mod);
	}
	return 0;
}
posted @ 2024-07-06 20:47  wangsiqi2010916  阅读(47)  评论(0)    收藏  举报