Cirno的邀请赛命题/题解报告

T1.Cirno的冰霜符文

难度:PJ-
这题是我在做题目P3722 [AH2017/HNOI2017]影魔
时候想出的Idea,感觉可以拿来凑一道签到题,作用是人口普查,
本来数据是2e6的,但是洛谷坑爹的20M限制,所以我只能把数据造到5e5.
然后这题很简单。
第二问直接左右扫一下即可。
第一问复杂一点,每次往左右暴力跳一格,然后跳到当前覆蓋点的记录的答案位置,可以证明每个点平均最多到达两次,然后就是严格O(n)的了。

STD:

#include<bits/stdc++.h>
using namespace std;

int n;
int h[2000005];
int pre [2000005][2];
int down[2000005][2];

void query(int now,bool flag){
    int ans = pre[now][flag];
    while( ans > 0 and ans <= n and h[ans] <= h[now] )ans = pre[ans][flag];
    if( ans == 0 or ans == n + 1 )ans = -1;
    pre[now][flag] = ans;
} 

int main(){
    std::ios::sync_with_stdio(false);
    cin >> n;
    for(int i = 1;i <= n;i ++)cin >> h[i];h[0] = h[n + 1] = 1e9;
    for(int i = 1;i <= n;i ++)pre[i][0] = i - 1,pre[i][1] = i + 1;
    for(int i = 1;i <= n;i ++)query( i,0 );
    for(int i = n;i >= 1;i --)query( i,1 );
    for(int i = 1;i <= n;i ++)down[i][0] = down[i][1] = i;
    for(int i = 1;i <= n;i ++)if( h[i] > h[i - 1] )down[i][0] = down[i - 1][0];
    for(int i = n;i >= 1;i --)if( h[i] > h[i + 1] )down[i][1] = down[i + 1][0];
    for(int i = 1;i <= n;i ++)printf("%d %d %d %d\n",pre[i][0],pre[i][1],down[i][0],down[i][1]);
    return 0;
}

T2 Cirno的数学课

难度: PJ
可以一眼秒掉的数学题,虽然我当时推了30min的式子。
不难看出某种规律 3i(i+1)n(n+1)(n+2) 3 i ∗ ( i + 1 ) n ∗ ( n + 1 ) ∗ ( n + 2 ) 是有隐含含义的。
先来复习一下高中数学的数列中的几个重要式子
ni ∑ n i = 1+2+3+…+n = n(n+1)2 n ∗ ( n + 1 ) 2
ni2 ∑ n i 2 =1+4+9+…+ n2 n 2 = n(n+1)(2n+1)6 n ( n + 1 ) ( 2 n + 1 ) 6
ni3 ∑ n i 3 =1+8+27+…+ n3 n 3 = n2(n+1)24 n 2 ( n + 1 ) 2 4
所以我们可以知道原数列 an=n(n+1)2 a n = n ( n + 1 ) 2 ,n项和为
Sn=n2+n2=(n(n+1)(2n+1)6+n(n+1)2)/2=n(n+1)(n+2)6 S n = ∑ n 2 + n 2 = ( n ∗ ( n + 1 ) ∗ ( 2 n + 1 ) 6 + n ∗ ( n + 1 ) 2 ) / 2 = n ∗ ( n + 1 ) ( n + 2 ) 6
ai=i(i+1)2 a i = i ∗ ( i + 1 ) 2
所以我们可以看出那个神奇的式子:
3i(i+1)n(n+1)(n+2)=aiSn 3 i ∗ ( i + 1 ) n ∗ ( n + 1 ) ∗ ( n + 2 ) = a i S n
所以数列中某个数字被选的概率是该数字除以该数列总和。
然后转化以下模型,可以理解为函数 f(x)=x(x+1)2 f ( x ) = x ∗ ( x + 1 ) 2 下的整数点都会被等概率的选取,然后选出的两个点(可以相同)第一个点纵座标大于第二个的概率。
然后可以很容易想到,第一个比第二个点大的概率的于第二个点比第一个点大的概率。所以只需要求出两点纵座标相等的概率即可。
然后考虑枚举每个横座标,发现都比前一个横座标多出来一些新的纵座标,具体地,对于第i个横座标,比前一个多出i个纵座标,然后后面(n-i)个数字都含有这个纵座标,然后直接发现相等方案数是 i(ni+1)2 i ∗ ( n − i + 1 ) 2 求和,总方案数是 S2n S n 2 ,然后就可作(做)了。
把一式展开,用代入前面的式子,然后用总方案数减去,然后除以2就大功告成了。
由于过程过于繁琐,这里就不再展开,最终的运算结果为:

12(13n(n+2)) 1 2 ( 1 − 3 n ( n + 2 ) )

然后就用费马小定理或者exgcd求一下逆元就轻松A了.

STD:

#include<bits/stdc++.h>
using namespace std;
const long long mod = 998244353;

long long f_pow(long long A,long long B){
    long long ans = 1;
    while( B ){
        if( B & 1 )( ans *= A ) %= mod;
        ( A *= A ) %= mod;
        B >>= 1;
    }
    return ans;
}

long long ns(long long A){
    return f_pow( A, mod - 2);
}

int main(){
    int n;cin >> n;
    if( n == 0 )return cout << ns(2),0;
    cout << ( ns(2) * ( 1 - 3ll  * ns(n) % mod * ns(n + 2) % mod ) % mod + mod ) % mod;
    return 0;
}

P.S. RedBag大佬当时一眼秒了%%%.

T3 Cirno的湖畔小屋

难度: TG
这是一道很随意的水题,是出题人闲着没事的时候想到的题目qwq,感觉很水然后就出出来了。
数据很大,不难想到离散化,区间赋值加查询,不难想到线段树,然后就很可做了。
(如果你是大佬就不用看后面的了).
由于修改都是从(0,0)开始的,所以不难想到没有覆蓋的轮廓是一个单调壳,每次新的覆蓋直接先找到最前面小于y的值的位置,然后区间查询总和,区间赋值,就可以轻松A掉了。其实还是很好想的,代码难度>思维难度,所以评级为TG吧。

#include<bits/stdc++.h>
using namespace std;

namespace segment{
    #define Lson ( now << 1 )
    #define Rson ( now << 1 | 1 )
    #define Ason ( p <= mid ? Lson : Rson )
    #define mid ( ( l[now] + r[now] ) >> 1 )
    #define Lrange Lson,L,min( mid,R )
    #define Rrange Rson,max( mid + 1,L),R
    #define Range(x) ( r[x] - l[x] + 1 )

    int line[100005];

    int l[400005];
    int r[400005];
    long long v[400005];
    int mi[400005];
    int lazy[400005];

    void build(int now,int L,int R){
        l[now] = L;r[now] = R;
        if( L == R )return ;
        build( Lson,L,mid );
        build( Rson,mid + 1,R );
    }

    void push_up(int now){
        v[now] = v[Lson] + v[Rson];
        mi[now] = min( mi[Lson],mi[Rson] );
    }

    void push_down(int now){
        int lz = lazy[now] ;lazy[now] = 0;
        if( !lz )return ;
        lazy[Lson] = lazy[Rson] = lz;
        mi[Lson] = mi[Rson] = lz;
        v[Lson] = 1ll * ( line[ r[Lson] ] - line[ l[Lson] - 1 ]  ) * lz ;
        v[Rson] = 1ll * ( line[ r[Rson] ] - line[ l[Rson] - 1 ]  ) * lz ;
    }

    int query(int now,int val){
        if( l[now] == r[now] )return l[now];
        push_down(now);
        return query( mi[Lson] <= val ? Lson : Rson, val );
    }

    long long modify(int now,int L,int R,int val){
        if( l[now] == L and r[now] == R ){ 
            long long ans = 1ll * ( line[ r[now] ] - line[ l[now] - 1 ] ) * val - v[now];
            v[now] += ans;
            mi[now] = val;
            lazy[now] = val;
            return ans;
        }
        push_down(now);
        long long Lans = 0,Rans = 0;
        if( L <= mid )Lans = modify( Lrange,val );
        if( R >  mid )Rans = modify( Rrange,val );
        push_up(now);
        return Lans + Rans;
    }
}

vector<int>que;
int line[100005][2];

int main(){
    int n;cin >> n;que.push_back(0);
    for(int i = 1;i <= n;i ++){
        cin >> line[i][0] >> line[i][1];
        que.push_back( line[i][0] );
    }
    sort( que.begin(),que.end() );
    auto Uend = unique( que.begin(),que.end() );
    int m = Uend - que.begin();
    segment::build( 1,1,m );
    for(int i = 1;i <= m;i ++ ){
        segment::line[i] = que[i];
    }
    for(int i = 1;i <= n;i ++ ){
        int x = lower_bound( que.begin(),Uend,line[i][0] ) - que.begin() ;
        int y = segment::query( 1,line[i][1] ) ;

        if( y > x ){ cout << "0\n"; continue; }
        cout << segment::modify( 1,y,x,line[i][1] ) << "\n";
    }
    return 0;
}

T4 Cirno的冰霜剑

难度:TG
似乎很水,这是出题人反复迭代过的一道题,原本是一个不可做的题目,然后就改成了这样,因为这个难度出题人终于能做出来了。由于出题是一个先有题目后又STD的过程,所以我也没有想到会转化成水题的模型,其实就是一个普普通通的费用流。
前面的预处理直接暴力是 O(n3) O ( n 3 ) 的,加点优化可以变成 O(n2lnn) O ( n 2 l n n ) 但这不是主要复杂度,本来出题人想在这个点上出题的,但是太水了。学过费用流的都知道接下来是水的。所以关于建模我就不再赘述了qwq.
STD:

#include<bits/stdc++.h>
using namespace std;

namespace SPFA{
    vector<int>to  [10005];
    vector<int>flow[10005];
    vector<int>cost[10005];
    vector<int>pair[10005];
    int Flow,Cost;
    int F,T;

    void init(int _f,int _t){
        F = _f;T = _t;
    }

    void add(int u,int v,int w,int c){
        pair[u].push_back( to[v].size() );
        pair[v].push_back( to[u].size() );
        to[u].push_back(v);
        to[v].push_back(u);
        flow[u].push_back(w);
        flow[v].push_back(0);
        cost[u].push_back(c);
        cost[v].push_back(-c);
    }

    bool SPFA(){
        static int len[10005];memset(len,1,sizeof(len));len[F] = 0;
        static int cap[10005];memset(cap,0,sizeof(cap));cap[F] = 1e9;
        static int from[10005],edge[10005];
        queue<int>Q;Q.push(F);
        while( !Q.empty() ){
            int now = Q.front();Q.pop();
            for(int i = 0;i < to[now].size();i ++){
                int next = to[now][i];
                if( !flow[now][i] )continue;
                if( len[now] + cost[now][i] >= len[next] )continue;
                len[next] = len[now] + cost[now][i];
                cap[next] = min( cap[now],flow[now][i] );
                from[next] = now;
                edge[next] = i;
                Q.push(next);
            }
        }
        if( !cap[T] )return false;
        int now = T;
        Flow += cap[T];
        Cost += cap[T] * len[T];
        while( now != F ){
            flow[ from[now] ][ edge[now] ] -= cap[T];
            flow[ now ][ pair[ from[now] ][ edge[now] ] ] += cap[T];
            now = from[now];
        }
        return true;
    }

    void get_ans(){
        while( SPFA() );
    }
}

int n;
char mp[5005][5005];
int cost[5005];

void get_cost(int line,int len){
    int now = 1;
    int ans = 0;
    while( now <= n ){
        while( now <= n and mp[line][now] == '0' )now ++;
        if( now > n )break;
        ans ++;now += len;
    }
    SPFA::add( line,len + 5000,1,ans * cost[len] );
}

int main(){
    cin >> n;
    SPFA::init( 10002,10003 );
    for(int i = 1;i <= n;i ++)scanf( "%s", mp[i] + 1 );
    for(int i = 1;i <= n;i ++)scanf( "%d", &cost[i] );
    for(int i = 1;i <= n;i ++)SPFA::add( 10002,i,1,0 ),SPFA::add( i + 5000,10003,1,0);
    for(int i = 1;i <= n;i ++)for(int j = 1;j <= n;j ++)get_cost(i,j);
    SPFA::get_ans();
    cout << SPFA::Cost ;
}

P.S.由于可以转模成原题,我称之为原题改编.

T5 Cirno的寒食节

难度:HAOI D1T1 ?(大雾
因为似乎HA省选最简单?然后这题难度似乎高出普及和提高,所以就和省选沾个边吧qwq.这也是这套水题的主要难度所在。
原题魔改(我的原话.
原题是HN集训还是YL集训,不记得去了,的某一场的T1,原题是一个可以Hash乱搞的简单字符串模式匹配题,当时就想到正解KMP的出题人表示很不满,于是把它改成了多模式串匹配,然后就顺理成章的用上了AC自动机,Hash乱搞的可以byebye了。当然这种允许置换的魔性字符串,AC自动机要魔改,具体地,fail指针的构造方式是不一样的。
然后我们从头讲起,不难想到,置换就是将原串改称某一个字符与前一个相同字符的距离,这就是原题正解。然后考虑出现第一次的字符,我们定义他为0,但是当他失配时,他后一个相同的字符就会变为0,所以在建立fail指针时要特殊照顾一下0的失配,具体的,记录一下某个点在自动机中的深度,然后回跳时判断自己的字符(已改成与前一个相同字符的距离)是否大于深度,否则就改为0.
然后匹配出所有的段,然后对所有段的两端点取出,然后扫描线上贪心的选灯笼,就可以AC了。
STD:

#include<bits/stdc++.h>
using namespace std;

struct node{
    int p,type;
    node(int _p,int _t) : p(_p), type(_t) {}
    bool operator <(const node A)const {
        return p < A.p;
    }
};
vector<node>line;
stack<int>unco;
bool co[100005];
bool in[100005];
int que[500005][2];
int pt;

namespace AC{
    map<int,int>to[500005];
    int fail[500005];
    int deep[500005];
    bool val[500005];
    bool mark[500005];
    int cnt;
    int k;

    void insert( int *s,int len ){
        int now = 0;
        for(int i = 1;i <= len;i ++){
            int v = s[i];
            if( i == s[i] + 1 )mark[now] = true;
            if( !to[now][v] )to[now][v] = ++cnt;
            int next = to[now][v];
            deep[next] = deep[now] + 1;
            now = next;
        }
        val[now] = true;
    }

    int find(int now,int next){
        while( now != 0 ){
            if( deep[now] < next )next = 0;
            if( to[now][next] )return to[now][next];
            now = fail[now];
        }
        if( to[now][next] )return to[now][next];
        return 0;
    }

    void build(){
        queue<int>Q;
        mark[0] = false;
        for(auto next = to[0].begin();next != to[0].end();next ++)Q.push( (*next).second );
        while( !Q.empty() ){
            int now = Q.front();Q.pop();
            for(auto next = to[now].begin();next != to[now].end();next ++){
                int v = (*next).first;
                int s = mark[now] ? 0 : v;
                fail[ (*next).second ] = find( fail[now],s );
                Q.push( (*next).second );
            }
        }
    }

    void query( int *s,int len ){
        int now = 0;
        for(int i = 1;i <= len;i ++){
            int v = s[i];
            now = find( now,v );
            for( int t = now;t != 0;t = fail[t]){
                if( val[t] )que[ ++ pt ][0] = i - deep[t] + 1;que[ pt ][1] = i;
            }
        }
    }
}

int partten[100005];

int last[200005];
int turn[200005];
int tpt = 0;
void deal(int *s,int len,int T){
    for(int i = 1;i <= len;i ++){
        int ka = s[i];
        if( turn[ka] != T )s[i] = 0,turn[ka] = T;
        else s[i] = i - last[ ka ];
        last[ ka ] = i;
    }
}

int main(){
    int n;cin >> n >> AC::k;
    for(int i = 1;i <= n;i ++)scanf( "%d", partten +  i );
    int t;cin >> t;
    while( t -- ){
        static int key[100005];
        static int len;scanf("%d",&len);
        for(int i = 1;i <= len;i ++)scanf( "%d", key + i );
        deal( key,len,++tpt );AC::insert( key,len );
    }
    AC::build();
    deal( partten,n,++tpt );AC::query( partten,n );
    for(int i = 1;i <= pt;i ++){
        line.push_back( node(que[i][0],i) );
        line.push_back( node(que[i][1],i) );
    }
    sort( line.begin(),line.end() );
    int ans = 0;
    for(int i = 0;i < ( pt << 1 );){
        int nowP = line[i].p;
        bool flag = false;
        while( line[i].p == nowP ){
            int nowT = line[i].type;
            if( in[nowT] and (!co[nowT]) )flag = true;
            in[nowT] = not in[nowT];
            if( in[nowT] )unco.push( nowT );
            i ++;
        }
        if( flag ){
            ans ++;
            while( !unco.empty() )co[unco.top()] = true,unco.pop();
        }
    }
    cout << ans;
}

后记

这场比赛,无疑是善良的出题人无私的馈赠。精心构造的各种随机的测试数据 能让程序中的错误无处遁形。出题人相信,这个美妙的比赛,可以给拼搏于AK其他官方比赛的逐梦之路上的你,提供一个有力的援助。

点赞