序列Counting
模拟的时候切掉的,感觉这道题还是很好的。(虽然T1期望啥也不会积分瞎搞拿了个二十五分,T2好不容易搞了个字符串dp,最后数组还开小了……不过终于狗进前五)
题面描述:
构建一个N个点的有向图G,初始没有任何边。接下来构建一个长度为E的边的序列A,序列中每条边都是满足1≤s,t≤N且s≠t的有向边(s,t),且序列中的边互不相同。按照顺序把这些边加入到G中,每次加入后计算当前图的强连通分量个数并记录下来,得到一个新的长度为E的正整数序列B。如果两个边的序列得到的B相同则称它们本质相同。
请问有多少种本质不同的边的序列,你只要求出答案对10^9+7取模后的结果。
很显然不能一遍一遍跑Tarjan(笑哭)
我们先分析一下这道题:
首先我们考虑一个合法的B序列,一定是单调不增的。我们将一条链作为图的主干考虑,对于我们加的每一条边,我们可以了考虑是继续拓展这条链,还是在原有的链的基础之上随便连边。那么常规的我们设计一个状态 f [ i ] [ j ] 表示前 i 条边,现在有 j 个强连通分量的方案数。
那么很显然对于 i 和 j 是有一些关 系的,不是任何一对 i , j 都是合法的。
那么我们考虑一波:
假设现在序列B被分为 k 段,每段的大小相等(连了一些废边),那么作为在链的基础上加的边至少有 k-1 条,那么也就是说在链上的边最多有 i - k +1条。
设Bi = j ,也就是说现在有 j 个强连通分量,那么显然用到链上的边最少为 n - j 条(一个环,j - 1 个孤立点,n - j + 1 个环上点,n - j + 1 条环上边,其中有一条反向的边)。
那么显然我们要满足 n - j <= i - k + 1 ,多、也就是说对于一对 i,j 我们必须满足 i + j >= n + k -1 才行。
这说明情况是否合法收到 k 的限制。也就是说对于一对 i , j ,其对应的情况并不都合法,i ,j 的合法情况是其所有情况的一个子集。
那么我们就要加一维限制,f [ i ] [ j ] [ k ] 代表对于加的 i 条边,有 j 个强连通分量,分成了 k 段的方案数。
然后我们又发现,由于这是简单图,所以当强连通分量个数为 j 时,边数是有上限的,最多为 ( n - j + 1 ) * ( n - 1 ) + ( j - 1 ) * ( j - 2 ) / 2 。
所以也必须满足条件 i <= ( n - j + 1 ) * ( n - 1 ) + ( j - 1 ) * ( j - 2 ) / 2 。
那么 f [ i ] [ j ] [ k ] 就很好转移了,只要考虑第 i 条边是否是废边即可 : f [ i ] [ j ] [ k ] += f [ i - 1 ] [ h ] [ k - 1 ] + f [ i - 1 ] [ j ] [ k ] 。
通过维护前缀和为我们可以把复杂度降到 n3,但是显然我们还是不满意的>///<
我们观察 i + j >= n + k -1 ,发现在 i >= 2n 的时候总是成立的,所以我们就不需要 k 这一维了,f [ i ] [ j ] 就可以了,转移也一样。
这样就大功告成啦!当然由于模拟的时候 n 是100,所以不需要后来的二维转移,三维n4就是可以接受的。
代码:
1.模拟代码:
#include <iostream> #include <cstdio> #define maxn 102 #define mod 1000000007 using namespace std; int dp[maxn * maxn][maxn][maxn]; int add(int x, int y) { x += y; return x >= mod ? x - mod : x; } int main() { int n; cin >> n; for (int i = n; i >= 0; -- i) dp[0][i][0] = 1; for (int e = 1; e <= n * (n - 1); ++ e) { int ans = 0; for (int v = 1; v <= n; ++ v) for (int m = 0; m < n; ++ m) if (e >= (n - v) + m && e <= (n - v + 1) * (n - 1) + (v - 1) * (v - 2) / 2) { dp[e][v][m] = add(dp[e][v][m], (mod + dp[e - 1][v][m] - dp[e - 1][v + 1][m]) % mod); dp[e][v][m] = add(dp[e][v][m], dp[e - 1][v + 1][m - 1]); ans = add(ans, dp[e][v][m]); } for (int m = 0; m <= n; ++ m) for (int v = n - 1; v >= 0; -- v) dp[e][v][m] = add(dp[e][v][m], dp[e][v + 1][m]); printf("%d ",ans); } return 0; }
2. n <= 400 二维优化代码:
#include<cstdio> #include<iostream> #include<queue> #include<algorithm> #include<cmath> #include<vector> #include<cstring> using namespace std; #define mod 1000000007 const int maxn = 500; int lim[maxn], f[2][maxn][maxn], sf[2][maxn][maxn], g[2][maxn], sg[2][maxn],ans[maxn*maxn]; int n; int main() { scanf("%d",&n); for (int i = 1; i <= n; i++) lim[i] = (n - i + 1) * (n - 1) + (i - 1) * (i - 2) / 2; f[1][n][1] = ans[1] = 1; for (int i = 1; i <= n; i++) sf[1][i][1] = 1; for (int i = 2; i <= min(n * (n - 1), n << 1); i++) { int op = i & 1; for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++) f[op][j][k] = 0; for (int j=1; j<=n; j++) if (i<=lim[j]) for (int k = 1; k <= n; k++) if (i + j >= n + k - 1) f[op][j][k] = (f[op ^ 1][j][k] + sf[op ^ 1][j + 1][k - 1]) % mod; for (int j = n; j >= 1; j--) for (int k = 1; k <= n; k++) { sf[op][j][k] = (sf[op][j + 1][k] + f[op][j][k]) % mod; ans[i] = (ans[i] + f[op][j][k]) % mod; } } for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++) g[0][j] = (g[0][j] + f[0][j][k]) % mod; for (int j = n; j >= 1; j--) sg[0][j] = (sg[0][j + 1] + g[0][j]) % mod; for (int i = (n << 1) + 1; i <= n * (n - 1); i++) { int op = i & 1; for (int j = 1; j <= n; j++) g[op][j] = 0; for (int j = 1; j <= n; j++) if (i <= lim[j]) g[op][j] = sg[op ^ 1][j]; for (int j = n; j >= 1; j--) { sg[op][j] = (sg[op][j + 1] + g[op][j]) % mod; ans[i] = (ans[i] + g[op][j]) % mod; } } for (int i = 1; i <= n * (n - 1); i++) printf("%d ", ans[i]); printf("\n"); return 0; }
注:感谢 yfl 和 lyn 以及 wty 大佬们的悉心讲解,第一题部分分积分算法已经明白:
对于一条链的情况,len(p)=sigama(i=1,边的个数)* sigama P * ( xi ) = sigama(i=1,边的个数)* 1/n * ( 0 + 1 ) * n / 2= ( n - 1 ) / 2 。