[Codeforces 814E] An unavoidable detour for home DP+BFS树+组合数学

题目链接   liu_cheng_ao大爷写的民间题解

注意: 以下内容打大部分都是对liu_cheng_ao大爷题解的转述(但是可能复杂度并没有那么优秀),

             因为博主太菜,并没有什么自己思考的能力(智力-=2)

题解: 因为每条边的权值相等,所以考虑bfs的过程,

             所以考察一张符合条件的图的bfs树的性质。

             (根据题意,我们把capital设为bfs树的树根)

             1. 显然,因为是bfs,

                 有边连接的两个点的深度差不超过1,

                 否则bfs中根到某个节点的距离就不是最短路了;

             2. 有了1这个很强的性质后,因为根到每个节点的最短路唯一,

                  所以bfs树上,相邻节点之间只能有1条边,

             根据1,2两点,我们可以发现,

             在bfs树上,非树边只能存在于深度相同的节点之间。

             又因为1到n这n个点到capital的距离递增,所以bfs树上,每一层的编号都是连续的一段。

             这样,就可以进行递推转移dp了:

             设f[i][j]表示考虑前i个位置,与第i个位置深度相同的点有j个,

             与i位置深度相同的点只考虑连接到父亲的边,深度小于第i个位置的所有点度数均已经满足的方案数。

             则可以通过枚举上一层的节点个数进行转移:

             dp of dp:f[i][j]=sigma(f[i-j][k]*g[j][c1][c2]).

                                其中,c1是上一层的度数为2的节点数,c2是上一层的度数为3的节点数,

                                g[i][j][k]表示这一层有i个节点,上一层有j个度数为2的节点,k个度数为3的节点,

                                假设上一层中的节点都有连向父亲的边的情况下,使得上一层中(j+k)个点的度数均满足条件的方案数。

                                g数组中,所有(j+k)个点两两不同。

                                g数组的转移: i=j=k=0情况下,g[i][j][k]=1.

                                                           i=j=0,k>0情况下,g[i][j][k]=sigma(l=3..k,g[i][j][k-l]*C(k-1,l-1)*N(l))

                                                           解释:考虑标号最大的点所在的环,

                                                                       因为每个点度数为2,形成若干个环,

                                                                       枚举的l是标号最大的点所在的环的点数,

                                                                       而N(l)是l个点形成的环的个数(点与点两两不同),

                                                                       N(l)=(l-1)!/2,即为项链数。

                                                           i=0,j>0情况下,g[i][j][k]=(j-1)*g[i][j-2][k]+k*g[i][j][k-1],

                                                           解释:考虑标号最大的度数为1的点所连向的边,

                                                                       此时,这个点可以向另一个度数为1的点进行连边,方案数为(j-1)*g[i][j-2][k].

                                                                                   这个点也可以向一个度数为2的点进行连边,方案数为k*g[i][j][k-1],

                                                                                   因为这么做使得度数为1的点的个数不变,度数为2的点的个数+1.

                                                           i,j,k>0情况下,g[i][j][k]=j*g[i-1][j-1][k]+k*g[i-1][j+1][k-1].

                                                           解释:考虑标号最大的儿子所连向的边,

                                                                       连向度数为2的点和连向度数为3的点分别讨论,

                                                                       若连向度数为2的点,这个点在本层内就无法再连了,

                                                                       若连向度数为3的点,这个点能连接到的边的条数从2变成了1,

                                                                       所以转移显然。

                               O(n^3)预处理出g数组后即可O(n^3)求出f数组得到最终答案。

             注意:f数组中每个点考虑时都认为其非根节点,所以根节点的情况提前处理一下,从第2层开始dp,

                         而对于叶子节点,统计答案时可以将其看做有0个儿子的非叶子节点进行转移。

                         (转移到dep[叶子节点]+1层时,叶子节点的度数限制就得到了满足)


Code:

#include <bits/stdc++.h>
#define ll long long
#define mod 1000000007ll
using namespace std;
/* g数组的计算: i!=0----->根据儿子连向的父亲计算,
                j!=0----->根据最后一个入度为2的点的连接方式计算
				k!=0----->根据最后一个点所在环的大小计算
				i=j=k=0----->g[i][j][k]=1.
   f数组的计算: f[d[1]+1][d[1]]=1
                f[i][j]=sigma(f[i-j][k]*g[j][c1][c2]).
   ans=sigma(f[n][i]*g[0][c1][c2]).
*/ 
ll c[51][51],p[51];
ll g[51][51][51];
ll f[51][51];
int a[51],n;
inline void get_c()
{int i,j;
for (i=0;i<=n;i++)
{c[i][0]=1ll;c[i][i]=1ll;
for (j=1;j<i;j++)
{c[i][j]=c[i-1][j]+c[i-1][j-1];
if (c[i][j]>=mod) {c[i][j]-=mod;}
}
}
p[3]=1;
for (i=4;i<=n;i++)
{p[i]=(p[i-1]*((ll)(i-1)))%mod;}
}
inline void get_g()
{int i,j,k;
for (j=0;j<=n;j++)
{for (k=0;k<=n;k++)
{if (j==0&k==0) 
{g[0][j][k]=1;continue;}
if (j>0)
{if (j-2>=0)
{g[0][j][k]+=((ll)(j-1))*g[0][j-2][k];
g[0][j][k]%=mod;
}
if (k-1>=0)
{g[0][j][k]+=((ll)(k))*g[0][j][k-1];
g[0][j][k]%=mod;
}
}
else
{for (i=3;i<=k;i++)
{g[0][j][k]+=((g[0][j][k-i]*c[k-1][i-1])%mod)*p[i];
g[0][j][k]%=mod;
}
}
}
}
for (i=1;i<=n;i++)
{for (j=0;j<=n;j++)
{for (k=0;k<=n;k++)
{if (j-1>=0)
{g[i][j][k]+=((ll)(j))*g[i-1][j-1][k];
g[i][j][k]%=mod;
}
if (k-1>=0)
{g[i][j][k]+=((ll)(k))*g[i-1][j+1][k-1];
g[i][j][k]%=mod;
}
}
}
}
}
inline void get_f()
{int i,j,k;
f[a[1]+1][a[1]]=1;
for (i=a[1]+2;i<=n;i++)
{for (j=1;j+a[1]+1<=i;j++)
{int c1=0,c2=0;
for (k=1;k+j<=i;k++)
{if (a[i-j-k+1]==2) {c1++;}
else {c2++;}
f[i][j]+=f[i-j][k]*g[j][c1][c2];
f[i][j]%=mod;
}
}
}
}
int main (){
	int i;
	scanf ("%d",&n);
	for (i=1;i<=n;i++)
	{scanf ("%d",&a[i]);}
	get_c();
	get_g();
	get_f();
	ll ans=0;
	int c1=0,c2=0;
	for (i=1;i<=n;i++)
	{if (a[n-i+1]==2) {c1++;}
	else {c2++;}
	ans+=f[n][i]*g[0][c1][c2];
	ans%=mod;
	}
	printf ("%lld\n",ans);
	return 0;
}
	

           

        

            

全部评论

相关推荐

上周组里招人,我面了六个候选人,回来跟同事吃饭的时候聊起一个让我挺感慨的现象。前三个候选人,算法题写得都不错。第一道二分查找,五分钟之内给出解法,边界条件也处理得干净。第二道动态规划,状态转移方程写对了,空间复杂度也优化了一版。我翻他们的简历,力扣刷题量都在300以上。后三个呢,就有点参差不齐了。有的边界条件没处理好,有的直接说这道题没刷过能不能换个思路讲讲。其中有一个女生,我印象特别深——她拿到题之后没有马上写,而是先问我:“面试官,我能先跟你确认一下我对题目的理解吗?”然后她把自己的思路讲了一遍,虽然最后代码写得不是最优解,但整个沟通过程非常顺畅。这个女生的代码不是最优的,但当我问她“如果这里是线上环境,你会怎么设计’的时候,她给我讲了一套完整的方案——异常怎么处理、日志怎么打、怎么平滑发布。她对这是之前在实习的时候踩过的坑。”我在想LeetCode到底在筛选什么?我自己的经历可能有点代表性。我当年校招的时候,也是刷了三百多道题才敢去面试。那时候大家都刷,你不刷就过不了笔试关。后来工作了,前三年基本没再打开过力扣。真正干活的时候,没人让你写反转链表,也没人让你手撕红黑树。更多的是:这个接口为什么慢了、那个服务为什么OOM了、线上数据对不上了得排查一下。所以后来我当面试官,慢慢调整了自己的评判标准。算法题我还会出,但目的变了。我出算法题,不是想看你能不能背出最优解。而是想看你拿到一个陌生问题的时候,是怎么思考的。你会先理清题意吗?你会主动问边界条件吗?你想不出来的时候会怎么办?你写出来的代码,变量命名乱不乱、结构清不清楚?这些才是工作中真正用得到的能力。LeetCode是一个工具,不是目的。它帮你熟悉数据结构和常见算法思路,这没问题。但如果你刷了三百道题,却说不清楚自己的项目解决了什么问题、遇到了什么困难、你是怎么解决的,那这三百道题可能真的白刷了。所以还要不要刷LeetCode?要刷,但别只刷题。刷题的时候,多问自己几个为什么:为什么用这个数据结构?为什么这个解法比那个好?如果换个条件,解法还成立吗?把刷题当成锻炼思维的方式,而不是背答案的任务。毕竟面试官想看到的,从来不是一台背题机器,而是一个能解决问题的人。
牛客51274894...:意思是光刷力扣还不够卷
AI时代还有必要刷lee...
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务