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\)能够产生它。
我真的看到这题的时候蒙了。唉,也没总结出什么有用的。
这种想象力只能靠刷题来累计。因为它本质就是你对于题目的预判,就和打游戏打多了对于对面什么时候要干什么都会清清楚楚的。这个也一样。
刷题吧。唉。
浙公网安备 33010602011771号