CF1027E Inverse Coloring 题解
CF1027E Inverse Coloring
本题解致力于分析其他大佬没有提到的一些简单的细节,以帮助像我一样的蒟蒻彻彻底底的理解这道题的做法
声明,本题所用\(dp\)方程与其他题解一致,因此不再赘述
按照设定,本题的初步\(dp\)值可以表示为以下形式:
1
1 2
1 3 4
1 5 7 8
1 8 13 15 16
...
分析转移关系可知,对于一个状态\(dp[i][j]\)的转移,它可以由所有的\(dp[k][min(k,j)]\)转移得到,\(k\)满足\((i-j\leq k\leq i-1)\)。
也就是各位大佬的式子:
让我们以\(dp[4][2]\)和\(dp[3][2]\)为例,看看它们的转移方式
将原式子拆出就会得到:
\(dp[3][2]=dp[1][1]+dp[2][2]\)
\(dp[4][2]=dp[2][2]+dp[3][2]\) (这里没有 \(dp[1][1]\) 是因为 \((k(1)<i-j(2)\) )
可以发现,对于每个位置的状态,都由前面的\((i-j\leq k\leq i-1)\)层之间的第\(min(k,j)\)项转移而来,说人话就是在限定的层数中,列数不大于当前列数的最后一项的dp值
有人可能不理解为什么这么加,按照我们设定的\(dp\)式子,来实际地模拟一下数塔的形成过程
注意:
这里只考虑以1开始的数,因此如果同时考虑从0开始的情况,其中的所有值都将乘2,这也是为什么最终结果要乘2。
我们扩展数列的规则为,将前面的层数中符合当前位置定义的数加以扩展。当我们由第i层的数扩展至第j层的数,则将第i层符合要求项的所有数列通过加同一数补满至长度为j的数列,且保证所补数与待补数的最后一项不同。例如:10->1011。
括号中是当前位置新拓展出的数,括号前的数为当前位置\((i,j)\)的\(dp\)值,每一项的符合要求的数列为同一层在不超过这一项的位置中括号里出现的数
括号中的数的数量实际就是\((i,j)\)位置,满足最长长度为\(j\)的方案数
1(1)
1(10) 2(11)
1(101) 3(110,100) 4(111)
1(1010) 5(1011,1100,1101,1001) 7(1110,1000) 8(1111)
···
由于我们所设的是,\(dp[i][j]\) 表示,对于长度为\(i\)的数列,最长连续字串不超过\(j\)的方案数,所以可以发现,对于每一层除第一项以外的项,其\(dp\)值均为前一项的数量加上当前位置新拓展出的数的数量,符合我们想要的容斥结果。
看看每一项是怎么转移的,还是以\(dp[4][2]\)为例,它拓展出的新数列可由第2层和第三层的数列得到,比如
10->1011
11->1100
101->1010
110->1101
100->1001
可以发现,除了第三个,其余都是(4,2)位置新拓展出的数。而第三个数显然也已经在\(dp[4][1]\)被拓展过了。
也就是说,\(dp[4][2]\)的拓展由第2层的全部,3层的前两项得到。而这其中就包含了我们前面得到转移方程中的两项限制\(dp[(i-j \leq k\leq i-1)][min(j,k)]\)
于是问题解决了,我们只要遍历每一层(\(i\))的每一项(\(j\))(其实只需遍历在题目所给的\(lim\)项之前的每一项即可),在更新当前项时,遍历第\(k(i-j\leq k\leq i-1)\)层,并加上当前层的第\(min(k,j)\)项即可,复杂度为\(O(n^3)\)。
CODE:
#include<bits/stdc++.h>
#define N 3005
#define ll long long
#define mod 998244353
#define f1(i,n,m) for(int i=n;i<=m;++i)
using namespace std;
int n,lim;
ll res;
ll dp[N][N],g[N],sum[N];
signed main(){
cin>>n>>lim;
f1(i,1,min(lim,n))dp[i][i]=1;
f1(i,2,n)f1(j,1,min(lim,i))f1(k,i-j,i-1)
dp[i][j]=(dp[i][j]+dp[k][min(j,k)])%mod;
f1(i,1,n)g[i]=(dp[n][i]-dp[n][i-1]+mod)%mod;
f1(i,1,n)f1(j,1,n)
if(i*j<lim)res=(res+g[i]*g[j])%mod;
printf("%lld",(res<<1)%mod);
return 0;
}
前缀和优化
每一项所加的值其实是固定的,我们只需要在算当前项的同时算前缀和就行,每个位置的前缀和其实就是上一层,同一列的前缀和加上当前位置的\(dp\)值。
当然,如果上一层的最后一列与比当前列位置靠前,直接加上一层,最后一列的前缀和即可。
想不明白可以看下面的数塔
1
2
3 4
4 7 8
5 12 15 16
6 20 28 31 32
该算法复杂度为\(O(n^2)\)
CODE:
#include<bits/stdc++.h>
#define N 3002
#define mod 998244353
#define f1(i,n,m) for(int i=n;i<=m;++i)
using namespace std;
int n,lim,res;
int dp[N][N],g[N],s[N][N];
signed main(){
cin>>n>>lim;
s[0][0]=1;
f1(i,1,n)f1(j,1,min(i,lim))
dp[i][j]=(s[i-1][min(i-1,j)]-(i-j-1<0?0:s[i-j-1][min(j,i-j-1)])+mod)%mod,//注意数组越界问题
s[i][j]=(s[i-1][min(i-1,j)]+dp[i][j])%mod;
f1(i,1,n)g[i]=(dp[n][i]-dp[n][i-1]+mod)%mod;
f1(i,1,n)f1(j,1,n)
if(i*j<lim)res=(res+1ll*g[i]*g[j])%mod;
printf("%lld",((1ll*res)<<1)%mod);
return 0;
}