题解—糖果
炒鸡神仙的dp题,在题解帮助下3天才做出来。
如果想看懂题解,关键的步骤是思考,愣在那干看很难懂或者比较深刻的理解。
这个题的思维有很多步,但大佬可以直接完成最后一步(%沈队)
Pre
一些定义
someone ‘ s 抉择排列 : 就是某人最终拿到的糖果和其拿到的糖果顺序,他的长度应该是n/3的。
答案序列 : 就是小C最终对答案有贡献的一个喜好程度的排列。
posa[i] 就是i在A中的位置,posb同理(posa[a[i]]=i,posb[b[i]]=i)。
Step 1
首先考虑,如果我们可以得到一个小C终得到的糖果和其拿到这些糖果的顺序,有多少种分配没被小C拿到的糖果的方案。
有两种得到答案的方式。
第一种
如果小C的抉择排列是合法的,那么AB也必定是合法的,换句话说就是 ABC 三人拿到的糖果构成全集。
那么,考虑把 AB 选的糖果塞到C的答案序列里面,最后C选择的就一定是一个排列。
倒序考虑塞的过程,为了避免分配失败,A,B的糖果一定要在C的这颗糖果后面。
假设现在是第\(i\)次塞,塞这个AB之前,小C已经完成了\(i-1\)次塞的过程。
也就是后面有\(3(i-1)\)个糖果,构成了 \(3(i-1)+1\)个空隙让我们可以往里面塞。
而塞进去一个后,空隙又多了一个,所以有\(3(i-1)+2\)个空隙可以塞。
以此类推,可以知道,如果确定一种小C抉择排列,有 \(\prod\limits_{i=1}^{n/3}(3*i-1)(3*i-2)\)种答案序列。
第二种
这个东西也可以通过 \(dp\) 得到
设计状态 \(f_{i,j}\) 代表考虑小C的前 \(i\) 个数,小 \(C\) 选了 \(j\) 个的时候(也就是当前进行到第 \(j\) 轮)的 对应的合法方案数
如果小C选择这个数,那么只有一种方案,因为我们前提已知的是小C的一个抉择排列,这个位置如果选,那么就唯一。
所以 有转移 \(f_{i,j} \rightarrow f_{i+1,j+1}\) 选择了一个数,所以多了一轮。
如果小C不选择这个数,才是我们\(dp\)的意义所在。
考虑小C为什么不选择这个数,原因肯定是之前就被AB中的一个人选走了。
而选择进行了 \(j\) 轮,AB一共选走了 \(2*j\)个数。
而之前,小C有\((i-j)\)个数也是经过而不选,也需要从AB的抉择排列里面找数。
所以,本次可以填写的数的方案就有 \(2*j-(i-j)=3*j-i\)种。
所以,有转移 \(f_{i,j} * (3*j-i) \rightarrow f_{i+1,j}\)
最终 \(f_{n,n/3}\) 就是答案。
Summary
虽然只是本题中最简单的第一步,仍然具有很多思想。
\(dp\),组合两种方法观察不同的性质,殊途同归。
对于这种题意比较隐晦的题,一定要先仔细读题观察几个性质,有用没用暂且不论。
完成这一步之后,我们只需要计算小C的合法抉择排列数即可。
参考代码
n = read() , f [0][0] = 1 , jc [0] = jc [1] = inv [0] = inv [1] = 1 ;
for( R j = 0 ; j <= n / 3 ; j ++ ) for( R i = 0 ; i <= 3 * j ; i ++ )
f [i + 1][j + 1] = A( f [i + 1][j + 1] , f [i][j] ) ,
f [i + 1][j] = A( f [i + 1][j] , T( f [i][j] , ( 3 * j - i ) ) ) ;
for( R i = 1 ; i <= n / 3 ; i ++ ) ans = T( ans , T( 3 * i - 1 , 3 * i - 2 ) ) ;
assert( f [n][n / 3] == ans ) ;
Step 2
然后考虑如何计算小C的抉择排列数
先从我自己推出来的并不对的 \(dp\) 开始
我设计 \(f_{i,j,k}\) 代表 A选到\(i\),B选到\(j\),C已经选了\(k\)个的时候的合法抉择排列数。
然后转移很简单,枚举A,B下一次加入自身抉择排列的数,\(i',j'\)
这里有一些限制
1. \(a[i']!=b[j']\)
2. \(a[i']\)不能在\(j'\)前出现(否则给不到A)
3. \(b[j']\)不能在\(i'\)前出现(否则给不到B)
满足这些限制的 \(i',j'\)才是合法的转移点。
然后考虑如何转移,就是考虑C在这个过程中多收获了几个数。
思考一会,不难发现如果这些数出现在 \([i+1,i’-1]\)中而A没有选,他不选的原因就是给了C
(因为已知B也没有选,因为我们也在枚举B的抉择排列,并且保证了转移点合法)
所以,可以发现,C多收获的数就是a序列中 \([i+1,i'-1]\)在b中 \([j+1,n]\)出现的数和b序列中 \([j+1,j'-1]\)在a中 \([i'+1,n]\)中出现的数
(此处不是 \([i+1,n]\)而是 \([i'+1,n]\)的原因就是防止 \([i+1,i'-1]\)重复计算)
假设上述中C多收获的数为\(t\)个。
那么我就以\(k\)的\(t\)次上升幂转移到\(f[i'][j'][k+t]\)(上升幂是因为往里面塞一个,能塞的空隙就多了一个)
然而这个是假的,这个是我通过数据调出来的,但是可以思考一下这个为什么假。
答案
因为给C的抉择排列里面增添t个数,并不能保证C能在A,B选择到这对数之前拿完。
如果还没理解的话,推荐打出来之后拍出来\(hack\)数据用数据理解。
所以需要改变状态定义,\(f_{i,j,k}\)代表 A选到\(i\),B选到\(j\),C还有\(k\)个备选位置的时候合法抉择排列数。
限制和 \(t\)的计算和上文一样,就是转移变了。
\(f_{i,j,k}\)以\(k\)的\(t\)次下降幂转移到 \(f_{i',j',k-t+1}\)
下降幂是因为放一个能放的缝隙就少了一个。
\(k-t+1\)是因为占据了\(t\)个备选位置,而AB选择了一对数,进而C也可以多选择一对数,所以备选位置又多了一个。
还要注意一个细节,就是有些抉择排列转移到一半因为没有下一个抉择点而不转移的情况,我们需要额外用 \(n+1\) 统计一下答案。(这个调试就可以知道了)
上述做法\(t\)的计算可以预处理,所以理论复杂度是 \(n^5\)的,因为刷表可以剪枝,所以有\(80pts\)
80 pts code
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <algorithm>
#define R register int
#define scanf Ruusupuu = scanf
#define freopen rsp_5u = freopen
#define fre(x) freopen( #x".in" , "r" , stdin ) , freopen( #x".out" , "w" , stdout )
int Ruusupuu ;
FILE * rsp_5u ;
using namespace std ;
typedef long long L ;
typedef double D ;
const int M = 1e3 + 10 ;
const int N = 4e2 + 10 ;
const int P = 1e9 + 7 ;
inline int A( int a , int b ){ return ( a + b ) >= P ? ( a + b - P ) : ( a + b ) ; }
inline int T( int a , int b ){ return ( 1ll * a * b ) % P ; }
inline int read(){
int w = 0 ; bool fg = 0 ; char ch = getchar() ;
while( ch < '0' || ch > '9' ) fg |= ( ch == '-' ) , ch = getchar() ;
while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch ^ 48 ) , ch = getchar() ;
return fg ? -w : w ;
}
int n , f [N][N] , dp [N][N][N] , ans = 1 , a [N] , b [N] , jc [M] , inv [M] ;
int prea [N][N][N] , preb [N][N][N] ; // prea [i][j][k] 表示 a [i] ~ a [j] 和 b [k] ~ b [n] 重合的数有多少个
int posa [N] , posb [N] , fga [N] , fgb [N] ;
void sc(){
n = read() , f [0][0] = 1 , jc [0] = jc [1] = inv [0] = inv [1] = 1 ;
for( R j = 0 ; j <= n / 3 ; j ++ ) for( R i = 0 ; i <= 3 * j ; i ++ )
f [i + 1][j + 1] = A( f [i + 1][j + 1] , f [i][j] ) ,
f [i + 1][j] = A( f [i + 1][j] , T( f [i][j] , ( 3 * j - i ) ) ) ;
for( R i = 1 ; i <= n / 3 ; i ++ ) ans = T( ans , T( 3 * i - 1 , 3 * i - 2 ) ) ;
assert( f [n][n / 3] == ans ) ;
for( R i = 1 ; i <= n ; i ++ ) a [i] = read() , posa [a [i]] = i ;
for( R i = 1 ; i <= n ; i ++ ) b [i] = read() , posb [b [i]] = i ;
for( R i = 2 ; i < M ; i ++ ) jc [i] = T( jc [i - 1] , i ) , inv [i] = T( ( P - P / i ) , inv [P % i] ) ;
for( R i = 2 ; i < M ; i ++ ) inv [i] = T( inv [i - 1] , inv [i] ) ;
for( R i = 1 ; i <= n ; i ++ ) for( R j = i ; j <= n ; j ++ ){
memset( fga , 0 , sizeof( fga ) ) , memset( fgb , 0 , sizeof( fgb ) ) ;
for( R k = i ; k <= j ; k ++ ) fga [a [k]] = fgb [b [k]] = 1 ;
for( R k = 1 ; k <= n ; k ++ ) for( R l = k ; l <= n ; l ++ ){
if( fga [b [l]] ) prea [i][j][k] ++ ;
if( fgb [a [l]] ) preb [i][j][k] ++ ;
}
}
}
void work(){
if( a [1] == b [1] ) return puts( "0" ) , void() ;
dp [1][1][0] = 1 ;
for( R i = 1 ; i <= n ; i ++ ) for( R j = 1 ; j <= n ; j ++ ) for( R k = 0 ; k <= n / 3 ; k ++ ){
if( !dp [i][j][k] ) continue ;
for( R ii = i + 1 ; ii <= n ; ii ++ ) for( R jj = j + 1 ; jj <= n ; jj ++ ){
int t = prea [i + 1][ii - 1][j + 1] + preb [j + 1][jj - 1][ii + 1] ;
if( k + 1 < t ) continue ;
if( a [ii] == b [jj] || posb [a [ii]] <= jj || posa [b [jj]] <= ii ) continue ;
int r = T( jc [k + 1] , ( k >= t + 1 ) ? inv [k - t + 1] : 1 ) ;
dp [ii][jj][k - t + 1] = A( dp [ii][jj][k - t + 1] , T( r , dp [i][j][k] ) ) ;
}
int t = prea [i + 1][n][j + 1] ;
if( k - t + 1 != 0 ) continue ;
int r = T( jc [k + 1] , ( k >= t + 1) ? inv [k - t + 1] : 1 ) ;
dp [n + 1][n + 1][0] = A( dp [n + 1][n + 1][0] , T( r , dp [i][j][k] ) ) ;
} printf( "%d\n" , T( ans , dp [n + 1][n + 1][0] ) ) ;
}
signed main(){
// fre( in ) ;
sc() ;
work() ;
return 0 ;
}
Step 3
考虑优化这个 \(n^5\) , 首先发现\(A,B\)的抉择相互影响可以消除,所以可以拆开两个\(dp\) 。
然后,和枚举下一个抉择点等价的方式就是枚举每个点选不选。
所以设状态定义\(f_{i,jk,0/1}\)代表 A选到\(i\),B选到\(j\),C还有\(k\)个备选位置的时候 ( \(0\)在选A , \(1\)在选B ) 合法抉择排列数。
转移的限制还是和 \(Step2\) 中一样。
来解释一下这6个转移,很多题解解释的都不对,但是如果 \(Step2\) 理解了,这个转移很好理解。
如果 \(posb[a[i]]<j\) ,那么说明这个数A一定无法选择,因为要不是B选了,要不是C选了。
所以有转移 \(f_{i,j,k,0}\rightarrow f_{i+1,j,k,0}\) 代表A不能选择这个数。
否则,A可以在这个时候考虑把这个数让自己选择或者让C选择。
让自己选择,那么此刻是A选了,B没有选择的状态,所以要把这个东西丢给B让B去转移
\(f_{i,j,k,0} \rightarrow f_{i,j,k,1}\)
让C选择,那么C的备选位置就少了一个,同时有k个选择位置,所以有转移
\(f_{i,j,k,0}*k \rightarrow f_{i+1,j,k-1,0}\)
注意这个\(f_{i,j,k,1}\)出现的唯一可能就是当前A选择了,而B暂时还没有选择。
所以B选择的时候,对于一个\(f_{i,j,k,1}\),他是知道A已经把 \(i\)选给自己的,所以B选择完之后要有如下转移
\(f_{i,j,k,1}\rightarrow f_{i+1,j+1,k+1,0}\),代表B选择完毕,所以两人的指针都要动一位。
其余转移和A类似。
AC code
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <algorithm>
#define R register int
#define scanf Ruusupuu = scanf
#define freopen rsp_5u = freopen
#define fre(x) freopen( #x".in" , "r" , stdin ) , freopen( #x".out" , "w" , stdout )
int Ruusupuu ;
FILE * rsp_5u ;
using namespace std ;
typedef long long L ;
typedef double D ;
const int M = 1e3 + 10 ;
const int N = 4e2 + 10 ;
const int P = 1e9 + 7 ;
inline int A( int a , int b ){ return ( a + b ) >= P ? ( a + b - P ) : ( a + b ) ; }
inline int T( int a , int b ){ return ( 1ll * a * b ) % P ; }
inline int read(){
int w = 0 ; bool fg = 0 ; char ch = getchar() ;
while( ch < '0' || ch > '9' ) fg |= ( ch == '-' ) , ch = getchar() ;
while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch ^ 48 ) , ch = getchar() ;
return fg ? -w : w ;
}
int n , f [N][N] , dp [N][N][N][2] , ans = 1 , res , a [N] , b [N] , jc [M] , inv [M] ;
int prea [N][N][N] , preb [N][N][N] ; // prea [i][j][k] 表示 a [i] ~ a [j] 和 b [k] ~ b [n] 重合的数有多少个
int posa [N] , posb [N] , fga [N] , fgb [N] ;
void sc(){
n = read() , f [0][0] = 1 , jc [0] = jc [1] = inv [0] = inv [1] = 1 ;
for( R j = 0 ; j <= n / 3 ; j ++ ) for( R i = 0 ; i <= 3 * j ; i ++ )
f [i + 1][j + 1] = A( f [i + 1][j + 1] , f [i][j] ) ,
f [i + 1][j] = A( f [i + 1][j] , T( f [i][j] , ( 3 * j - i ) ) ) ;
for( R i = 1 ; i <= n / 3 ; i ++ ) ans = T( ans , T( 3 * i - 1 , 3 * i - 2 ) ) ;
assert( f [n][n / 3] == ans ) ;
for( R i = 1 ; i <= n ; i ++ ) a [i] = read() , posa [a [i]] = i ;
for( R i = 1 ; i <= n ; i ++ ) b [i] = read() , posb [b [i]] = i ;
for( R i = 2 ; i < M ; i ++ ) jc [i] = T( jc [i - 1] , i ) , inv [i] = T( ( P - P / i ) , inv [P % i] ) ;
for( R i = 2 ; i < M ; i ++ ) inv [i] = T( inv [i - 1] , inv [i] ) ;
}
void work(){
if( a [1] == b [1] ) return puts( "0" ) , void() ;
dp [1][1][0][0] = 1 ;
for( R i = 1 ; i <= n ; i ++ ) for( R j = 1 ; j <= n ; j ++ ) for( R k = 0 ; k <= n / 3 ; k ++ ){
if( !dp [i][j][k][0] ) goto ZT ;
if( posb [a [i]] < j ) dp [i + 1][j][k][0] = A( dp [i + 1][j][k][0] , dp [i][j][k][0] ) ;
else{
dp [i][j][k][1] = A( dp [i][j][k][1] , dp [i][j][k][0] ) ;
if( k ) dp [i + 1][j][k - 1][0] = A( dp [i + 1][j][k - 1][0] , T( k , dp [i][j][k][0] ) ) ;
}
ZT : ;
if( !dp [i][j][k][1] ) continue ;
if( posa [b [j]] < i ) dp [i][j + 1][k][1] = A( dp [i][j + 1][k][1] , dp [i][j][k][1] ) ;
else if( b [j] != a [i] ){
dp [i + 1][j + 1][k + 1][0] = A( dp [i + 1][j + 1][k + 1][0] , dp [i][j][k][1] ) ;
if( k ) dp [i][j + 1][k - 1][1] = A( dp [i][j + 1][k - 1][1] , T( k , dp [i][j][k][1] ) ) ;
}
} for( R i = 1 ; i <= n ; i ++ ) res = A( res , dp [n + 1][i][0][0] ) ;
printf( "%d\n" , T( ans , res ) ) ;
}
signed main(){
// fre( in ) ;
sc() ;
work() ;
return 0 ;
}
</details>