第八周南宁周赛题解(临时赶的)
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 2∗1e5/i的范围内,时间复杂度依然是过大了,但是砍掉了 n / m x n/mx n/mx之后的切割操作,就会尽可能降低了set的空间和遍历的次数。
而对于重复度较高的值,我们mx接近1,但是set的复杂度是 o ( 1 ) o(1) o(1) ,这就刚好能用 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[i−1],别问我原因,就是公式爆推出来的,没有太大技巧,做多了就熟能生巧了
一堆取模都是防爆破,这题蛮容易炸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]+mod−res)%
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 广西师范
鸣谢
算法竞赛解析:矩阵快速幂、枚举算法与字符串处理
这篇博客介绍了近期一场算法竞赛的解题思路,涉及斐波那契数列的矩阵快速幂求解、后缀操作的枚举算法、字符串构造与分割问题。博主详细讲解了每道题目的核心思想,并给出了相应的C++代码实现,探讨了时间复杂度优化策略。
279

被折叠的 条评论
为什么被折叠?



