Codeforces Round 1035 (Div. 2) D反思

D

​ 定义数列\(a\)合法当且仅当对于\(\forall 1\leq i \leq n 都有 0\leq a_i \leq i\).

对于一个长度为\(n\)的合法数列\(a\),定义其价值\(f(a)\)为:

  • 初始,在数轴的\(1\)\(n\)的整数位置上,各放有一个标记。
  • 沿着这个数组,进行\(n\)次操作。第\(i\)次操作,如果\(a_i \not = 0\),就在区间\([a_i,i]\)内选择一个没有被移除的标记,移除它。否则,什么也不做。
  • \(f(a)\)就是移除标记的方式的数量。我们认为两个移除的方式不一样,当且仅当存在一个\(t\),使得这两个方式中第\(t\)次操作是不同的。

现在给你两个数字\(n,m\),要求计算对于所有\((n+1)!\)个长度为\(n\)的合法序列,\(f(a)\)是多大,并把答案对\(m\)取模。

数据保证\(1\leq n\leq5000,10^8\leq m\leq1.01 \cdot10^9\)

思路

如果真的按照题面描述的这样来,那我们考虑的过程就是,先单独考虑每个序列,再对每个序列考虑答案,再加起来。

简化的思路肯定是从最外层循环做的,就是考虑我们枚举的相邻的两个排列的价值之间有何联系。那么更需要做的就是分析求解\(f(a)\)的过程应该是怎么样的,从中去提取用于"考虑我们枚举的相邻的两个排列的价值之间有何联系"的特点。

好的,现在我们的思路来到了如何计算\(f(a)\),以及解构其中可以用于相邻或者是相似排列间价值计算转化的特质。

如何计算\(f(a)\)

\(f(a)\)是什么?移除标记的方式数量总和。方式数量总和巨大,绝对不可能去枚举统计。其实这个时候感觉就会告诉我用dp。这是很合理的,确实只能用dp。除了dp,剩下的只有数学规律可能能把这个算出来 。但是这个东西的结构似乎不满足我已知的任何数学模型。所以也不可能是数学规律方面的。那思路便只有dp了。

我们考虑如何用dp计算。
\(f(a)\)是什么出发来设计状态。\(dp[i]\)表示完成前\(i\)个操作的方案数。
好的,想到这里,后效性就体现出来了。由于我们并不知道某个位置到底取走了什么标记,相对应的,后面的\(a\)能够取走的标记的数量也是不确定的。而这,是和我们的答案统计极度相关的。
而普通的状态设计完全没法满足这个需求。

怎么办?

我的赛时就卡死了。因为不知怎么办。

那么,下面的部分就是我个人需要学习的思路了。

首先,上面的所有一切,在我的思路看来,只可能是两种情况

  • 状态错了。
  • dp是不可行的。

无后效性以这种"拿取物品顺序"的方式爆炸了,我做过的题目里面没有对于这种情况的让我印象深刻的处理方式。所以我也没法设计出一个我觉得合理的状态,相应的,也不知道dp到底可不可行。

说实话,想象力这一块,真的很重要。我想象不到这题的某些位置可能会拥有什么性质,而拥有什么性质能够让我把这题以某种方式做出来。所以我没有思路。这也是我菜的原因吧。

这个时候应该产生的思路不应该只有上面两个。我还应该去知道还有一种可能性。也就是属于这题的解法。

我赛时能够寻找到的启发性应该只有一个,就是每个\(a_i\)只能属于\([0,i]\)这个区间。那么其实就意味着,标记的选取存在一种包含的关系。也就是第\(i\)个标记只能够在排列\([i,n]\)这些位置里面选择拿走。

那么假如我们转换一下思路,固定排列中某个位置只能去拿某一个标记,是不是我们直接就可以知道这个位置的排列的数值有几种可能?
但是问题又来了。我们钦定了目前的某一个位置取走某一个标记,我们依旧逃不开记录前面有那些空缺以计算价值。这一点依旧无法解决。

这也是这一题很重要的一个地方。我们需要发现,其实当我们确定要取走某个标记\(x\)的时候,当前这个位置\(a\)只能是\([1,x]\)的某个值。而这其实可以说是这个标记\(x\)的代价。

我们只需要提前处理好这个代价,然后就可以将其简单的抽象为"一个未填的位置",再记录到状态中。

那么这样,dp就成立了。但是还不够,因为我们现在对于一个数组\(a\),要计算它的答案,还需要去枚举我们是否最终需要某个标记。还是非常麻烦。

但是我们回过头看看,我们真的有必要去对于某一个特定的\(a\)做这种事情吗?
其实可以调换一下顺序。我们去对于每一个方案,有多少种排列可以作为可以产生它的数列\(a\)
那基于我们之前的考虑,其实一个tag最终是否被拿走,在具体数列的第几个位置被拿走,对于产生的方案数没有影响。
我们是不是可以直接用\(f[i][j]\)表示前\(i\)个数字都已经考虑过是否出现过,前面还有\(j\)个数字需要填,但是没有预留空位的时候,所有方案的贡献的总和是多少。

接下来就是题解内的部分

假如我们保持还有\(j\)个空位,那就是三种情况。
1.我们当前的要出现,然后从这\(j+1\)个空里面选一个摸走。那么方案数是对于这\(j+1\)个需要取的都是一个方案数。\(f[i][j]+=f[i-1][j]*(j+1)*i\)

2.我们当前的要出现,但是我们这个位置a是0,于是我们让空位数量+1 。\(f[i][j]+=f[i-1][j-1]*i\)

3.我们当前的不要出现,那要么这里是0,要么就是依旧从前面挑一个摸走。\(f[i][j]+=f[i-1][j+1]*(j+1)+f[i-1][j]\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
	ll a=0,b=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
	for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
	return a*b;
}
ll f[5001][5001];
int main()
{
	int T=read();
	while(T--)
	{
		ll n=read(),m=read();
        for(int i=1;i<=n;i++)
		{
			for(int j=0;j<=n;j++)
			{
				f[i][j]=0;
			}
		}
		f[0][0]=1;
		for(ll i=1;i<=n;i++)
		{
			for(ll j=0;j<=n;j++)
			{
				if(j!=0)f[i][j]=(f[i][j]+f[i-1][j-1]*(i)%m)%m;
				f[i][j]=(f[i][j]+f[i-1][j]*((j+1)*i+1)%m)%m;
				if(j!=n)f[i][j]=(f[i][j]+f[i-1][j+1]*(j+1)%m)%m;
				
			}
		}
		cout<<f[n][0]%m<<endl;
	}
	return 0;
}

说实话,感觉分析的依托。

说到底,这题我做不出来的主要原因有两个
1.没想到如果我们确定了要拿走一个tag,它无论什么时候拿产生的方案数是确定的。
2.基于上面的,调换枚举的顺序,考虑对于每个排列,计算有多少数列\(a\)能够产生它。

我真的看到这题的时候蒙了。唉,也没总结出什么有用的。

这种想象力只能靠刷题来累计。因为它本质就是你对于题目的预判,就和打游戏打多了对于对面什么时候要干什么都会清清楚楚的。这个也一样。

刷题吧。唉。

posted @ 2025-07-09 18:25  Tracer_w  阅读(63)  评论(0)    收藏  举报