「CEOI2020」象棋世界

题目

点这里看题目。


给定一个 \(R\times C\) 的棋盘,满足 \(R\ge C\)。棋盘上的行按照国际象棋棋盘的编号规则,从下到上将行编号为 \(1,2,3,\dots,R\),从左到右将列编号为 \(1,2,3,\dots,C\)。第 \(r\) 行与第 \(c\) 列交叉产生的格子记为 \((r,c)\)

给出 \(Q\) 次询问,每次询问给出一种棋子和参数 \(c_1,c_R\),问若棋子从 \((1,c_1)\) 走到 \((R,c_R)\) 且不走出棋盘,需要的最小步数及在步数最小的前提下的方案数,对 \(10^9+7\) 取模。

棋子一共有 \(5\) 种,分别为 Pawn, Rook, Queen, Bishop, King。走子方式参考国际象棋。

所有数据满足 \(1\le C\le R\le 10^3,1\le Q\le 10^3\)

分析

提示:这道题里没有“马”

前三种棋子的计算过程都比较简单,这里先略去不讲。主要说一说 Bishop 和 King 的计算方法。

Bishop

很明显,Bishop 可以从 \((1,c_1)\) 走到 \((R,c_R)\),当且仅当两个格子的颜色相同,也就是 \(c+1\equiv R+c_R\pmod 2\)

关键在于计算过程。贪心地想,如果棋盘充分宽,理论上我们至多只需要走两步。如果棋盘不够宽,贪心思路受限而退化成“走到边界后换向”,反复“弹跳”。容易发现,每一步“走得尽量远”是满足全局最优性的局部贪心策略,因此这种贪心思路没有问题。当然,我们需要枚举起始方向。

问题是,我们最终不一定可以准确地走到 \((R,c_R)\)。考虑一个更弱的限制,我们必须先走到第 \(c_R\) 列上,再考虑调整到第 \(R\) 行上。因为前面的步骤都是贪心前进,所以我们只能考虑超出第 \(R\) 行的情况。假如第 \(c_R\) 列上,第 \(X\) 行是满足 \(X\ge R\) 且可以到达的最小编号行,则我们需要在前面的某些步提前转向

每提前一步,会导致行数减少 \(2\),因此总共需要提前 \(\frac{X-R}{2}\) 次。假如最小转向次数为 \(t\),我们该如何分配?我们可以不可以任意分配到某一次转向上?这个猜测可以由后续的分析支撑(关键在于,有没有信心去分析!),所以方案数为 \(\dbinom{t-1+\frac{X-R}{2}}{t-1}\)

顺便,枚举完第一步方向后,最小转向次数可以直接算出来,不谈。另外,若两个方向的最小步数相同,则两个方向的答案需要加起来

Note.

注意一些通用的策略:弱化/强化限制猜!结!论!

King

这一部分更加机械化,反而简单一点。

首先,King 的最小步数就是两个位置间的 Chebyshev 距离 \(\max\{|c_1-c_R|,R-1\}=R-1\)

相应地,King 的每一步都会向上走一行,因此我们可以对于方案数直接做 DP。进一步地,这个 DP 的转移式是关于第二维的线性运算,我们写出转移矩阵:

\[A_n= \begin{bmatrix} 1&1&0&\dots&0&0&0\\ 1&1&1&\dots&0&0&0\\ 0&1&1&\dots&0&0&0\\ \vdots&\vdots&\vdots&\ddots&\vdots&\vdots&\vdots\\ 0&0&0&\dots&1&1&1\\ 0&0&0&\dots&0&1&1 \end{bmatrix} \]

现在我们就需要求出 \(A_C^{R-1}\),且没有后退的余地。


快速求矩阵幂有成套的方法,一般而言我们都需要先研究矩阵的特征。

\(A_C\) 一个海森堡矩阵,而且对称性很好。相应地,\(\lambda I-A_C\) 也有这样的性质,所以计算 \(\det(\lambda I-A_C)\) 时我们可以策略地使用 Laplace 展开。具体过程略过,总之我们可以得到:

\[f_n(\lambda)=(\lambda -1)f_{n-1}(\lambda)-f_{n-2}(\lambda),n\ge 2 \]

其中 \(f_n(\lambda)\)\(A_n\) 的特征多项式,边界为 \(f_0(\lambda)=1,f_1(\lambda)=\lambda-1\)

手算较小项,发现 \(n=3\) 的时候特征值就已经很抽象了 😢。所以,目前看来这条路只能引导我们到一个方向:利用 Cayley Hamilton 定理,将 \(A_C^{R-1}\) 降成 \(A_C\) 的较低次幂的线性组合。系数就是 \(\lambda^{R-1}\bmod f_{C}(\lambda)\) 的系数,到此为止都可以 \(O(C^2\log R)\) 计算。

Remark.

事实上可以做到更好。因为无论是 \(f_C(\lambda)\),还是 \(\lambda^{R-1}\bmod f_C(\lambda)\),都是常系数齐次线性递推的形式,所以我们完全可以套用已有方法。

具体一点,\(f\) 的递推式很短,所以我们可以暴力维护递推过程,它的计算只受限于多项式运算。\(\lambda^{R-1}\bmod f_C(\lambda)\) 本身就是已经解决的问题。

于是,这里可以做到 \(O(C\log C\log R)\)

怎么求出 \(A_C\)\(0,1,2,\dots,C-1\) 次幂?由于 \(A_C\) 形式特殊,单次矩阵乘法可以直接优化到 \(O(C^2)\)。即便如此,这个过程仍然是 \(O(C^3)\) 的,不尽如人意。

注意到这些幂次有很强的组合背景,我们应当尝试结合组合意义,建立一个递推关系。幂次之间的递推是平凡的,我们考虑同幂次内部的关系。

具体地,考虑 \((1,x)\rightarrow (r,y)\) 的过程,其中 \(r<C\)。很容易注意到,\((1,x)\rightarrow (r,y)\)\((1,x\pm 1)\rightarrow (r,y\pm 1)\) 非常相近,我们“几乎”可以通过平移实现转换。

“几乎”之外的是,路径可能会卡到边界,引起方案数的变化。例如,从 \(p_1:(1,x-1)\rightarrow (r,y-1)\) 平移到 \(p_2:(1,x)\rightarrow (r,y)\) 时,\((A_C^{r-1})_{x,y}\) 会比 \((A_C^{r-1})_{x-1,y-1}\) 多算“\(p_2\) 恰好经过第 \(1\) 列”的方案数,少算“\(p_1\) 恰好经过第 \(n\) 列”的方案数。

怎么处理这些边界情况?两侧边界不好搞(容易直接挂钩“反射容斥”),我们先想办法去掉一侧。注意到移动步数很少,我们可以想到一条路径最多经过第 \(1\) 列和第 \(C\) 列之一,绝不可能同时经过这两列,也就是有一侧边界是完全无效的。我们不妨认为第 \(1\) 列是有效的边界——这就意味着 \(x-1+y-1\le r\)——并且考察方案数的变化。

此时,\((A_C^{r-1})_{x,y}\) 仅会多算而不会少算(可以验证,\(p_1\) 不可能接触到第 \(C\) 列),而多算的部分,通过翻折变换,可以发现等于 \((1,x) \rightarrow (r,2-y)\) 的路径数。进一步平移后变为 \((1,x+y-1)\rightarrow (r,1)\) 的路径(具体过程有待深究)。因此,我们有:

\[(A_C^{r-1})_{x,y}=(A_C^{r-1})_{x-1,y-1}+(A_C^{r-1})_{x+y-1,1} \]

如果仍然把握不大,可以用归纳法来进行后续证明

这样,当 \(r<C\) 时,我们得到第一行即可推出整个矩阵。再利用运算的线性性,我们可以将这个结论推广到所有幂次上。所以,我们可以 \(O(m^2)\) 的求出需要的 \(A_C^{R-1}\)

Remark.

值得玩味:发现目标的过程不一定就是证明目标的过程,那样太死板了。一旦确定了目标,我们的一切数学工具都可以开始尝试介入了。

代码

#include <cmath>
#include <vector>
#include <cstdio>

#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

const int INF = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int MAXN = 1e3 + 5;

template<typename _T>
inline void Read( _T &x ) {
    x = 0; char s = getchar(); bool f = false;
    while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
    while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
    if( f ) x = -x;
}

template<typename _T>
inline void Write( _T x ) {
    if( x < 0 ) putchar( '-' ), x = -x;
    if( 9 < x ) Write( x / 10 );
    putchar( x % 10 + '0' );
}

template<typename _T>
inline _T Min( const _T &a, const _T &b ) {
    return a < b ? a : b;
}

template<typename _T>
inline _T Max( const _T &a, const _T &b ) {
    return a > b ? a : b;
}

typedef std :: vector<int> Poly;
typedef void ( *SingleSolve ) ();

int mat[MAXN][MAXN];
int dp[MAXN][MAXN];
int eig[MAXN][MAXN];
int coe[MAXN];

int charId[300];

int R, C, Q;

#define Color( x, y ) ( ( (x) + (y) ) % 2 )

inline int Qkpow( int, int );
inline int Inv( const int &a ) { return Qkpow( a, mod - 2 ); }
inline int Mul( int x, const int &v ) { return 1ll * x * v % mod; }
inline int Sub( int x, const int &v ) { return ( x -= v ) < 0 ? x + mod : x; }
inline int Add( int x, const int &v ) { return ( x += v ) >= mod ? x - mod : x; }

inline int& MulEq( int &x, const int &v ) { return x = 1ll * x * v % mod; }
inline int& SubEq( int &x, const int &v ) { return ( x -= v ) < 0 ? ( x += mod ) : x; }
inline int& AddEq( int &x, const int &v ) { return ( x += v ) >= mod ? ( x -= mod ) : x; }

inline int Qkpow( int base, int indx ) {
    int ret = 1;
    while( indx ) {
        if( indx & 1 ) MulEq( ret, base );
        MulEq( base, base ), indx >>= 1;
    }
    return ret;
}

inline Poly operator * ( const Poly &a, const Poly &b ) {
    int n = a.size(), m = b.size();
    Poly ret( n + m - 1, 0 );
    rep( i, 0, n - 1 ) rep( j, 0, m - 1 )
        AddEq( ret[i + j], Mul( a[i], b[j] ) );
    return ret;
}

inline Poly operator % ( Poly a, const Poly &b ) {
    if( a.size() < b.size() ) return a;

    int n = a.size(), m = b.size();
    Poly rm( m - 1, 0 );
    per( i, n - 1, m - 1 ) if( a[i] ) {
        int coe = Mul( a[i], b[m - 1] );
        rep( j, 0, m - 1 ) SubEq( a[i + j - m + 1], Mul( coe, b[j] ) );
    }
    rep( i, 0, m - 2 ) rm[i] = a[i];
    return rm;
}

inline int Binom( const int &n, const int &m ) {
    int ret = 1;
    rep( i, 0, m - 1 )
        MulEq( ret, Mul( n - i, Inv( i + 1 ) ) );
    return ret;
}

void SolvePawn() {
    int c1, c2;
    Read( c1 ), Read( c2 );
    if( c1 == c2 ) {
        Write( R - 1 ), putchar( ' ' );
        Write( 1 ), putchar( '\n' );
    } else puts( "0 0" );
}

void SolveRook() {
    int c1, c2;
    Read( c1 ), Read( c2 );
    if( c1 == c2 ) puts( "1 1" );
    else puts( "2 2" );
}

void SolveQueen() {
    int c1, c2;
    Read( c1 ), Read( c2 );
    if( c1 == c2 || c2 == c1 - R + 1 || c2 == c1 + R - 1 )
        puts( "1 1" );
    else {
        int ans = 4;
        // 条件限制保证了 |c2-c1| <= R-1
        // 必然为纵向长条形
        if( c1 - R + 1 >= 1 ) ans ++;
        if( c2 - R + 1 >= 1 ) ans ++;
        if( c1 + R - 1 <= C ) ans ++;
        if( c2 + R - 1 <= C ) ans ++;
        if( Color( 1, c1 ) == Color( R, c2 ) ) {
            int y = ( R - 1 + c2 - c1 ) >> 1,
                x = ( R - 1 - c2 + c1 ) >> 1;
            if( c1 - x >= 1 ) ans ++;
            if( c1 + y <= C ) ans ++;
        }
        Write( 2 ), putchar( ' ' );
        Write( ans ), putchar( '\n' );
    }
}

void SolveBishop() {
    int c1, c2;
    Read( c1 ), Read( c2 );
    if( Color( 1, c1 ) != Color( R, c2 ) ) {
        puts( "0 0" );
        return;
    }
    int t1, t2, k1, k2, x1, x2;
    // 向左
    k1 = ceil( .5 * ( R - 1 - c1 + c2 ) / ( C - 1 ) );
    k2 = ceil( .5 * ( R + 1 - c1 - c2 ) / ( C - 1 ) );
    if( k1 <= k2 ) t1 = k1 << 1, x1 = 2 * k1 * ( C - 1 ) + c1 - c2 + 1;
    else t1 = k2 << 1 | 1, x1 = 2 * k2 * ( C - 1 ) + c1 + c2 - 1;

    k1 = ceil( .5 * ( R - 1 - c2 + c1 ) / ( C - 1 ) );
    k2 = ceil( .5 * ( R - 1 - 2 * C + c1 + c2 ) / ( C - 1 ) );
    if( k1 <= k2 ) t2 = k1 << 1, x2 = 2 * k1 * ( C - 1 ) + c2 - c1 + 1;
    else t2 = k2 << 1 | 1, x2 = 2 * k2 * ( C - 1 ) + 2 * C - c1 - c2 + 1;

    int t = Min( t1, t2 ), ans = 0;
    x1 = ( x1 - R ) >> 1, x2 = ( x2 - R ) >> 1;
    if( t == t1 ) AddEq( ans, Binom( x1 + t - 1, x1 ) );
    if( t == t2 ) AddEq( ans, Binom( x2 + t - 1, x2 ) );
    
    Write( t + 1 ), putchar( ' ' );
    Write( ans ), putchar( '\n' );
}

void SolveKing() {
    int c1, c2;
    Read( c1 ), Read( c2 );
    Write( R - 1 ), putchar( ' ' );
    Write( mat[c1][c2] ), putchar( '\n' );
}

const SingleSolve Each[] = { SolvePawn, SolveRook, SolveQueen, SolveBishop, SolveKing };

inline void Init() {
    eig[0][0] = 1;
    eig[1][0] = mod - 1, eig[1][1] = 1;
    rep( n, 2, C ) rep( j, 0, n ) {
        eig[n][j] = Mul( mod - 1, Add( eig[n - 1][j], eig[n - 2][j] ) );
        if( j ) AddEq( eig[n][j], eig[n - 1][j - 1] );
    }

    Poly eigPoly( C + 1, 0 );
    Poly bas = { 0, 1 }, res = { 1 };
    rep( i, 0, C ) eigPoly[i] = eig[C][i];
    bas = bas % eigPoly;
    for( int x = R - 1 ; x ; x >>= 1 ) {
        if( x & 1 ) res = res * bas % eigPoly;
        bas = bas * bas % eigPoly;
    }
    dp[0][1] = 1;
    rep( i, 1, C - 1 ) rep( j, 1, C ) {
        dp[i][j] = dp[i - 1][j];
        if( j > 1 ) AddEq( dp[i][j], dp[i - 1][j - 1] );
        if( j < C ) AddEq( dp[i][j], dp[i - 1][j + 1] );
    }
    rep( i, 1, C ) { 
        rep( j, 0, C - 1 )
            AddEq( mat[1][i], Mul( res[j], dp[j][i] ) );
        mat[i][1] = mat[1][i];
    }
    rep( i, 2, C ) rep( j, 2, C + 1 - i )
        mat[i][j] = Add( mat[i - 1][j - 1], mat[1][i + j - 1] );
    rep( i, 1, C ) rep( j, C + 2 - i, C )
        mat[i][j] = mat[C + 1 - i][C + 1 - j];
}

int main() {
    charId[( int ) 'P'] = 0;
    charId[( int ) 'R'] = 1;
    charId[( int ) 'Q'] = 2;
    charId[( int ) 'B'] = 3;
    charId[( int ) 'K'] = 4;

    Read( R ), Read( C ), Read( Q );
    Init();
    while( Q -- ) {
        char buf[10]; scanf( "%s", buf );
        Each[charId[( int ) buf[0]]]();
    }
    return 0;
}
posted @ 2022-10-27 22:09  crashed  阅读(184)  评论(0编辑  收藏  举报