「NOI2022」移除石子

题目

对于一个长度为 \(n\) 的正整数序列 \(\{a\}\) 和参数 \(K\),你可以进行如下三种操作:

  1. 选择一个 \(1\le i\le n\),满足 \(a_i\ge 2\),并选取一个 \(2\le k\le a_i\),令 \(a_i\gets a_i-k\)

  2. 选择一个连续的编号区间 \([l,r]\) 满足 \(1\le l,l+2\le r\le n,\min_{l\le i\le r}a_i>0\),令 \(l\le i\le r\) 的每一个 \(a_i\gets a_i-1\)

  3. 选择一个 \(1\le i\le n\),令 \(a_i\gets a_i+1\)

第三种操作必须在前两种操作之前进行,且必须恰好进行 \(K\)。而进行完第三种操作之后,你可以按任意顺序执行任意多次前两种操作。如果存在一种操作的方式,使得 \(\{a\}\) 可以变成一个全 \(0\) 的序列的话,我们就说 \(\{a\}\) 是“好”的序列。

现在,给定 \(n\)\(K\),以及 \(n\) 对参数 \(\{(L_i,R_i)\}\)。如果我们允许 \(a_i\)\([L_i,R_i]\) 中任意整数,则所有的可能的 \(\{a\}\) 中,有多少个是“好”的?答案对 \(10^9+7\) 取模。

对于 \(100\%\) 的数据,满足 \(3\le n\le 1000,0\le K\le 100, 0\le L_i\le R_i\le 10^9\)

多组数据,单个测试点数据组数不超过 \(10\) 组。

分析

GG 了,一上来连 DP 套 DP 都没想到......

25 pts

你这出题人给分也太吝啬了。

首先考虑,怎么进行 \(L_i=R_i\) 的检查?我最开始的想法是针对序列本身进行讨论,这可以在 \(R_i\le 2\) 的时候生效,但深入下去就很恼火了。

想一想为什么?直接讨论的困难在于,可能的操作方案实在是太多了。凭借直觉构造无异于盲目搜索,更何况我向来不擅长这种东西。因此,我们首先需要做的应当是将操作归约到某些特殊情况上

从这个角度入手,我们不难想到如下几个事实:

Conclusion.

  1. 任意操作二的编号区间长度 \(\le 5\)

  2. 不存在两次操作二操作的编号区间完全一样。


Proof.

  1. \(\ge 6\) 的,对半剖开一定可以变成两个不短于 \(3\) 的。

  2. 如果出现了两次以上对于 \([l,r]\) 的操作,我们可以转而对于 \(l\le i\le r\) 的每一个 \(a_i\) 执行相同数量的操作一。

Note.

这种复杂操作的问题,首先研究操作的特性、归约的方法应该是基础的思路。尤其需要关注多种操作互相配合产生的效果

我们的目标就应该是,尽量把涉及到的操作变短、变少、变简洁

现在,从每个位置起始的操作二只有至多三次(对应长度为 \(3,4,5\) 的操作区间),而操作一非常灵活。因此,我们可以想到确定操作二的情况进行检查,而实际上从前往后扫描时,我们只需要知道较近的若干个操作二的情况即可。

因此,可以设计 DP:\(f_{i,a,b,c,d}\) 表示到第 \(i\) 个位置时,\(i-4\sim i-1\) 的操作二区间个数分别为 \(a,b,c,d\) 时,前缀 \(i\) 能否被清除完。进一步地,由于转移过程只需要考虑多少个操作二区间被 terminate 了,\(a,b,c\) 实际上是本质相同的,状态可以被改写成 \(g_{i,e,d},e=a+b+c\)

很明显我们需要关注 \(e,d\) 的范围。现在是 \(0\le e\le 9,0\le d\le 3\)。更深入的分析表明范围可以缩小,但在这个部分里已经够用了。

40 pts

你这出题人给分也太吝啬了。

此时需要考虑 \(K\) 的影响。直觉是,\(K\) 和“好不好”应该存在单调性,但是(原)题面似乎否定了这一点。那么,我们就来看一下单调性的问题。

单调性其实就是看“好的序列再执行一次操作三是否仍然好”,指向的是操作的调整。从这一点入手,我们可以得出:

Conclusion.

当且仅当 \(n\) 和“好”的序列 \(\{a\}\) 满足下列条件之一时,对于 \(\{a\}\) 多进行一次操作三总会导致 \(\{a\}\) 变得不“好”:

  1. \(n=3,\{a\}=\{1,1,1\}\)

  2. \(\{a\}=\{0\}_{i=1}^n\)


Proof.

正向的分析。

首先,如果某种操作方案中存在操作一,我们可以直接在这个操作一的位置上进行操作三,然后相应地增加操作一的 \(k\)

其次,如果某种操作方案中存在操作二,且对应的 \([l,r]\neq [1,n]\),我们可以在 \(l-1\) 或者 \(r+1\) 处进行操作三,然后扩展操作二的区间。

现在,可能变不“好”的序列只有全 \(1\) 的序列和全 \(0\) 的序列。在 \(n>3\) 时,对于全 \(1\) 的序列而言,我们可以对于 \(a_1\) 进行操作三,然后添加一次针对 \(a_1\) 的操作一,最后进行一次 \([2,n]\) 的操作二。

最后就可以得到上面的结论。

Note.

具体操作的核心思路是:对于已有的方案执行微调。在已有的方案上面施工是便利的,轻易抛弃它常常是愚蠢的。

也就是说,我们可以毫不费力地将 \(g\) 改成对于“最小进行操作三次数”的 DP。最后只需要额外判断一下两种不合单调性的情况即可。

55 pts

现在进入了计数问题,我们也自然地想到了 DP 套 DP 的思路。

问题是,即便是布尔 DP,\(g_i\) 也有足足 \(40\) 个值,我们需要压缩。

第一步压缩比较简单。如果以某个位置出发的操作二区间真的有 \(3\) 个,我们其实可以通过操作二,将长度为 \(4,5\) 的切掉一部分:

1 ---       1 ---
2 ----   => 2 |---
3 -----     3 |----

横线表示操作二区间,竖线表示操作一区间。

那么,\(g_{i,e,d}\)\(e,d\) 可以被压缩到 \(0\le e\le 6,0\le d\le 2\),总共 \(18\) 个值。如果常数充分小,跑一跑 \(2^18\) 的 DP 说不定可以过(。

再压缩就很困难了。\(d\) 似乎已经走到了尽头,但是 \(e\) 真的需要这么大吗?实际上,

Note.

个人认为,这里的难点在于“想到并相信 \(e\) 的上界可以继续收缩”。这有点反直觉,但是我们必须看明白这样一个事实,就是 \(e\) 是一个合并过的变量,\(d\) 是单个的变量。\(d\le 2\) 可能是紧的,但是 \(e\le 6\) 可能还是松的,也必须意识到多个操作组合起来威力巨大。

想到这一点之后,后续的检查其实相对简单。为什么呢?因为有了参考答案,可以对拍验证

最终,我们可以压缩到 \(0\le e\le 2,0\le d\le 2\)。此时跑 DP 套 DP 就不难了。

注意在这个范围下,\(a\) 实际上只需要枚举到 \(6\)\(\ge 6\)\(a\) 没有本质区别。

100 pts

是的,这个部分分分布实在是太糟糕了。

此时内层的 \(g\) 是对于“最小值”的 DP。从暴力的角度来考虑,我们仍然可以执行先前的 DP 套 DP,只不过现在的内层状态达到了 \((K+2)^9\) 之巨。

现在你会想什么?放弃这个做法,还是 tricky 地使用 std :: map 来存放状态?

肯定选择后者啊!明显可以和之前的东西兼容嘛,而且容易从 55pts 的代码改过来,写起来肯定容易。写对了就好了,过不过得了根本不归我管。

相当有效。实际上,可能遇到的状态只有 \(8000\sim 9000\) 个。这个做法可以进一步优化——我们提前将所有状态搜出来,顺便把转移表也打出来,这就相当于建立了一个 DFA,之后对着转移即可。

Note.

DP 内层实际上就是装了一个 DFA。而这告诉我们,看起来状态数很多的 DFA,实际上能用的状态可能根本没多少

因此,如果再遇到一个很大的 DFA,我们应当尝试搜索一下它的有效状态再评估思路的可行性。

代码

 #include <map>
#include <cstdio>
#include <vector>
#include <cstring>

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

typedef long long LL;

const int mod = 1e9 + 7;
const int MAXN = 1e3 + 5, MAXS = 9e3 + 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;
}

std :: map<LL, int> mp;
int tsf[MAXS][9], ntot = 0;
bool imp[MAXS];

int f[2][MAXS];
int dp[2][3][3];

int L[MAXN], R[MAXN];

int N, K;

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 bool TestAllZero() {
    rep( i, 1, N )
        if( L[i] > 0 )
            return false;
    return K == 1;
}

inline bool TestAllOne() {
    rep( i, 1, N )
        if( R[i] < 1 || 1 < L[i] )
            return false;
    return N == 3 && K == 1;
}

int DFS( const LL hsh ) {
    if( mp.find( hsh ) != mp.end() ) return mp[hsh];
    int cur = ++ ntot; 
    imp[mp[hsh] = cur] = ( hsh % ( K + 2 ) ) <= K;
    rep( v, 0, 8 ) {
        LL tmp = hsh;
        rep( i, 0, 2 ) rep( j, 0, 2 ) {
            dp[0][i][j] = tmp % ( K + 2 ), tmp /= K + 2; 
            dp[1][i][j] = K + 1;
        }
        rep( a, 0, 2 ) rep( b, 0, 2 ) {
            if( dp[0][a][b] > K ) continue;
            rep( p, 0, a ) rep( r, 0, 2 ) if( a - p + b <= 2 ) {
                int t = v - a - b - r;
                dp[1][a - p + b][r] = Min( dp[1][a - p + b][r], dp[0][a][b] + Max( - t, 0 ) + ( t == 1 ) );
            }
        }
        tmp = 0;
        per( a, 2, 0 ) per( b, 2, 0 )
            tmp = tmp * ( K + 2 ) + dp[1][a][b];
        tsf[cur][v] = DFS( tmp );
    }
    return cur;
}

int main() {
    int T; Read( T );
    while( T -- ) {
        Read( N ), Read( K );
        rep( i, 1, N ) Read( L[i] ), Read( R[i] );
        ntot = 0, mp.clear(); LL beg = 0;
        per( i, 8, 1 ) beg = beg * ( K + 2 ) + K + 1;
        DFS( beg * ( K + 2 ) );
        rep( i, 1, ntot ) f[0][i] = f[1][i] = 0;
        int pre = 1, nxt = 0; AddEq( f[nxt][1], 1 );
        rep( i, 1, N ) {
            pre ^= 1, nxt ^= 1;
            rep( j, 1, ntot ) if( f[pre][j] ) {
                for( int k = L[i] ; k <= R[i] && k < 8 ; k ++ )
                    AddEq( f[nxt][tsf[j][k]], f[pre][j] );
                if( R[i] >= 8 ) AddEq( f[nxt][tsf[j][8]], Mul( f[pre][j], ( R[i] - Max( L[i], 8 ) + 1 ) % mod ) );
                f[pre][j] = 0;
            }
        }
        int ans = 0;
        rep( j, 1, ntot ) if( imp[j] && f[nxt][j] )
            AddEq( ans, f[nxt][j] );
        if( TestAllZero() ) SubEq( ans, 1 );
        if( TestAllOne() ) SubEq( ans, 1 );
        Write( ans ), putchar( '\n' );
    }
    return 0;
}
posted @ 2022-09-03 10:57  crashed  阅读(182)  评论(0编辑  收藏  举报