南宁周赛8题解

算法竞赛解析:矩阵快速幂、枚举算法与字符串处理
这篇博客介绍了近期一场算法竞赛的解题思路,涉及斐波那契数列的矩阵快速幂求解、后缀操作的枚举算法、字符串构造与分割问题。博主详细讲解了每道题目的核心思想,并给出了相应的C++代码实现,探讨了时间复杂度优化策略。

第八周南宁周赛题解(临时赶的)

A 类斐波那契求值

题目不难,矩阵快速幂板子+递推推论 算是结论题,只要板子没写错

大概有这么几个容易错的点,第一,初始化的e矩阵是斜对角为1;第二,最好用自定义类型装矩阵,可以实现输出值交换,而且写法接近正常情况。第三,0的时候没考虑。不过数据好像没考虑0。

递推的话我推荐一个博客大家去看看这里的结论:
矩阵快速幂求斐波那契数列

不过稍稍有一点点修改,看我的代码就知道了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1e9+7;
ll n, a, b;
struct mat
{
    ll a[2][2];
};
mat mat_mul ( mat x, mat y )
{
    mat res;
    memset ( res.a, 0, sizeof ( res.a ) );
    for ( int i = 0; i < 2; i++ )
        for ( int j = 0; j < 2; j++ )
            for ( int k = 0; k < 2; k++ )
                res.a[i][j] = ( res.a[i][j] + x.a[i][k] * y.a[k][j]%MOD ) % MOD;
    return res;
}
void mat_pow ( ll n )
{
    mat c, res;
    c.a[0][0] = a,c.a[0][1] =b, c.a[1][0] = 1;
    c.a[1][1] = 0;
    memset ( res.a, 0, sizeof ( res.a ) );
    for ( int i = 0; i < 2; i++ ) res.a[i][i] = 1;
    while ( n )
    {
        if ( n & 1 ) res = mat_mul ( res, c );
        c = mat_mul ( c, c );
        n = n >> 1;
    }
    printf ( "%lld\n", res.a[0][0] );
}
int main ()
{

    cin >> n >> a >> b;
    if ( n == 0 )
        cout << 0 << endl;
    else if ( n == 1 )
        cout << 1 << endl;
    else
        mat_pow ( n-1 );
    
    return 0;
}

B后缀操作

这题是枚举算法的应用,原题是在cf上的,验题组有人刷到过。

时间选太近了,下次选个老古董的题 XD

具体枚举是这样的,对于所有操作开始之前的特殊操作,你可以认为是把某个数字挖掉。

99 96 97 95 ,你可以把其中的一个数字挖走,然后剩下的数字进行差的绝对值加和。具体有证明过程,我只负责讲思路。

因为你很容易想到这题的所谓后缀改变,其实是差值不变,所以无所谓从后往前或者从前往后,也就很容易想到差的绝对值就是操作次数。

接下来就是考虑这个变换操作,其实变换任何数字,都只对相邻的操作有影响,而不影响前后任何操作。那么只要进行暴力计算附近的贡献值即可。直到找到一个改变该值,贡献最大的选项。

思路讲完了,直接贴代码+证明

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
//ll dp[5];
ll const mod=1e9+7;
const int N=2e5+6;
//ll h1[200005],h2[200005];
ll sum[N],x[N],sum2[N];
ll n,m;
ll ans=0;
ll mx;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x[i];
        sum[i]=abs(x[i]-x[i-1]);
        //ans+=sum[i];
    }
    for(int i=1;i<n;i++){
        ans+=sum[i+1];
    }
    if(n==1||n==2){
        cout<<0;
        return 0;
    }
//    ll ans=0;
    mx=ans-sum[2];
    for(int i=3;i<=n;i++){
        ll tmp=(sum[i-1]+sum[i]);
        ll tmp2=abs(x[i-2]-x[i]);
        mx=min(mx,ans-tmp+tmp2);
    }
    mx=min(mx,ans-sum[n]);
    cout<<mx;
}

证明过程

C 构造字符串

牛客寒假训练赛做到了。经典递推算法。

我们可以从头开始想,这个子序列出现d 出现p 和出现dp,和啥都没出现的情况。

第一个字符我们可以认为是出现了d 和出现了剩下25个不同的字符。

第二个字符我们可以认为是出现了p,d和其他24个不同的字符。

已知d必须在p的前面,如果出现了p但是前面是d,那么dp组成了。

如果p前面不是d,那么p和别的字符也没啥两样。

如果又出现了d,那么跟我p也没啥关系。

那么状态就开始清晰起来了,无d,有d无p,有d有p三种情况

无d想要转化为有d,那就是从前一个无d,再获得一个d。

有d无p想要有p,那就从前一个有d无p,再获得一个p。

有d有p的所有值,都可以乘26取模,因为他已经组成了dp。

最后要把所有dp的结果都加起来,最好用滚动dp优化一下,这样空间复杂度直线下降

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
ll dp[5];
//int x[4];
const int mod = 1e9 + 7;
ll n;
ll ans = 0;
int main() {
    cin >> n;
    dp[0] = 1;
    for (int i = 1; i <= n; i++) {
        dp[2] = (dp[2] * 26 + dp[1]) % mod;//有d有p
        dp[1] = (dp[1] * 25 + dp[0]) % mod;//有d无p
        dp[0] = dp[0] * 25 % mod;//无d
        ans = (ans + dp[2]) % mod;//加和
    }
    cout << ans;

}

D分割完美子串

这题最妙的点在于时间复杂度,有人特地问我这里的时间复杂度,我自己其实也推过了时间复杂度,感觉怎么样都会爆,直到我看了一眼标程,发现少了一步优化就对了。

首先是hash,这题是经典单hash前后缀同时优化的字符串hash问题。

因为 a b ab ab 等于 b a ba ba 的性质,所以如果只算前缀,是无法得到 a b = = b a ab==ba ab==ba这个结论的,因此要利用后缀去再算一遍hash, 第二步就是利用set查重。

最后也是最经典的一步,已知最大个数是mx,那也就是说,如果超过了 n / m x n/mx n/mx 的切割操作都是无意义的,可以砍掉一堆时间复杂度,如果是重复度比较低的值,set的复杂度将达到 l o g ( n ) log(n) log(n)甚至更高,尽管每次切割的次数都在 2 ∗ 1 e 5 / i 2*1e5/i 21e5/i的范围内,时间复杂度依然是过大了,但是砍掉了 n / m x n/mx n/mx之后的切割操作,就会尽可能降低了set的空间和遍历的次数。

而对于重复度较高的值,我们mx接近1,但是set的复杂度是 o ( 1 ) o(1) o1 ,这就刚好能用 n n n\sqrt{n} nn 低空飞过。

可能时间复杂度计算依然有点问题,但是大致上不会出现太大问题了(有问题也不是我背锅,我只是个临时工)

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
//ll dp[5];
ll const mod = 1e9 + 7;
ll h1[200005], h2[200005];
string s;
vector<int >ve;
ll p[200006];
ll hash1(int l, int r) {
    return (h1[r] - h1[l - 1] * p[r - l + 1] % mod + mod) % mod;
}
ll hash2(int l, int r) {
    return (h2[l] - h2[r + 1] * p[r - l + 1] % mod + mod) % mod;
}
set<ll>st;
int n, m;
int main() {
    cin >> s;
    n = s.length();
    for (int i = 0; i < n; i++) {
        h1[i + 1] = (h1[i] * 2333 + s[i] - 'a' + 1) % mod;
        h2[n - i] = (h2[n - i + 1] * 2333 + s[n - i - 1] - 'a' + 1) % mod;
    }
    p[0] = 1;
    for (int i = 1; i <= n + 5; i++)p[i] = (p[i - 1] * 2333) % mod;
    ll mx = 1;
    for (int i = 2; i <= n / mx; i++) {//here is the mx
        int t = 0;
        for (int j = i; j <= n; j += i) {
            ll ss = hash1(j - i + 1, j);
            ll se = hash2(j - i + 1, j);
            // for(int k=j-i;k<j;k++){
            //     cout<<s[k];
            // }
            // cout<<"\n";
            // cout<<se<<" "<<ss<<"\n";
            // cout<<"-------------------\n";
            if (!st.count(ss) && !st.count(se)) {
                st.insert(ss), st.insert(se);
                t++;
            }
        }
        if (mx < t) {
            ve.clear();
            mx = t;
            ve.push_back(i);
        }
        else if (mx == t)    ve.push_back(i);


    }
    cout << mx << "\n";
    for (auto i : ve) {
        cout << i << " ";
    }


}

E 冲动的小x

这题是公式,我临时推的,印象里以前做到过。反正每次计算都是常数查询

技巧就是前缀和应用

s u m 1 sum1 sum1 是正经的前缀和, s u m 2 [ i ] = s u m 1 [ i ] ∗ a [ i ] + s u m 2 [ i − 1 ] sum2[i]=sum1[i]*a[i]+sum2[i-1] sum2[i]=sum1[i]a[i]+sum2[i1],别问我原因,就是公式爆推出来的,没有太大技巧,做多了就熟能生巧了

一堆取模都是防爆破,这题蛮容易炸long long 的亚子

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
//ll dp[5];
ll const mod=1e9+7;
const int N=1e5+6;
//ll h1[200005],h2[200005];
ll sum[N],x[N],sum2[N];

ll n,m;
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>x[i];
        sum[i]=(sum[i-1]+x[i])%mod;
        sum2[i]=(sum2[i-1]+x[i]*sum[i])%mod;
    }
    int l ,r;
    while(m--){
        cin>>l>>r;
        cout<<((sum[r]-sum[l-1])*sum[r]%mod-(sum2[r]-sum2[l-1])+2*mod)%mod;
        puts("");
    }
}

F 模数意义下的最优删除

这题算半个结论吧,已知序列之和取模为res,删除一段连续子序列之和取模为res的最小序列即可。

一听暗示很明显了,依然是前缀和,只不过这次是前缀和+取模运算。而且要考虑到取模运算后的前缀可能 l > r l>r l>r,所以要多加取模值防爆破。

第二个问题,如何利用前缀和来优化时间复杂度。我们想到的方案是利用当前的前缀和,去推测他需要减去的前缀和是否出现过,因为取模后大家都一样,所以
( s u m [ r ] + m o d − r e s ) (sum[r]+mod-res) (sum[r]+modres)% m o d = s u m [ l ] mod=sum[l] mod=sum[l]% m o d mod mod 成立

接下来是优化方案,我跟出题人好像是想到一块去了,都是利用map去存前缀和的点。

我们可以很显然的想到一个问题,如果从前往后找,如果我们记录下来所有的点就代表了这个前缀是否存在,存在就可以直接提取出他的地址来进行个数运算。

如果有相同的数字那肯定是覆盖的好。可以自己挨想想我就不证明了。

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
//ll dp[5];
ll const mod = 1e9 + 7;
ll sum[200005], x[200005];
map<ll, int>mp;
int n, m;
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> x[i];
        sum[i] = (sum[i - 1] + x[i]) % m;
    }
    ll res = sum[n] % m;
    if (res == 0) {
        cout << 0;
        return 0;
    }
    ll ans = -1;
    for (int i = 0; i <= n; i++) {
        ll tmp = (sum[i] - res + m) % m;
        if (mp.count(tmp) && (ans == -1 || ans > i - mp[tmp])) {
            ans = i - mp[tmp];
        }
        mp[sum[i]] = i;
    }
    if (ans == n)cout << -1;
    else cout << ans;
}

G 棋子移动

签到题,直接看代码不想说了

#include<bits/stdc++.h>
using namespace std;
int x[4];
int main(){
    cin>>x[0]>>x[1]>>x[2];
    sort(x,x+3);
    if(x[0]==x[1]-1&&x[1]==x[2]-1)cout<<0;
    else if(x[0]>x[1]-3||x[1]>x[2]-3)cout<<1;
    else cout<<2;
}

这次是哪个人出的题,说好的AK专场呢???

个人感觉平均难度cf 1500,AK专场不至于,毕竟主力全去打银川了。

出题组:GXNU 广西师范

鸣谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值