2025河北师范大学校赛题解

        考虑到群体是全校,总体来看这次题目简单题部分还是偏简单了。简单题目大部分都是思维题,只有man!考到了简单的bfs。

难度分布

总共12道题目,以下是题目的难度分布:

very-easy 签到:A.河北师大最靓的仔、L.老丁的幸运手机号。

easy : J.叮当的奇妙大数、H.man!

mid: F.洞穴逃生、K.学长的圣遗物。

mid-hard: B.选择排序、G.省电、D.一道简单的数学题easy-version。

hard:C.唐山皇帝。

防ak:E.一道简单的数学题hard-version、I.小孙无法思考。

一血名单

下面是一血名单:

A.河北师大最靓的仔 (徐锦杰)

B.选择排序(李建川)

C.唐山皇帝 (无)

D.一个简单的数学题esay version (孙浩滨)

E.一个简单的数学题hard version (金鳞玲)

F.洞穴脱险 (孙传炜)

G.省电 (孙传炜)

H.man! (孙传炜)

I.小孙无法思考 (无)

J.叮当的奇妙大数 (宗意)

K.学长的圣遗物 (李延栋)

L.老丁的幸运手机号 (李金聪)

接下来题解按照难度递增给出。

very-easy部分(签到)

河北师大最靓的仔

题面

思路

        经典的思维诈骗输出题。其实只要输出“XXX”即可,如果没有输出”XXX“那么我们后台会看到你们会输出了谁的名字,v我50不泄密。

代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
    cout << "XXX" << endl;
    return 0;
}

老丁的幸运手机号

题面

思路

        简单题,只需要更出现8更多的字符串,没有出现更多的我就不更新,这样就满足题意,看代码。

代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
	int n;
	cin >> n;
	int maxn = -1;
	string res;
	for(int i = 1; i <= n; i ++)
	{
		int cnt = 0;
		string s;
		cin >> s;
		for(auto t: s)
		{
			if(t == '8') cnt ++;
		}
		if(cnt > maxn)
		{
			maxn = cnt;
			res = s;
		}
	}
	cout << res << endl;
}

easy部分

叮当的奇妙大数

题面

思路

        依旧是诈骗题。其实我们发现即使是9999拆开也只能是99 * 99诸如此类的乘法,始终不可能大于原来的数字。所以我们只需要输出原来数字各个位相加的结果就好了。

代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
	int n;
	cin >> n;
	string s;
	cin >> s;
	int res = 0;
	for(int i = 0; i < n; i ++)
	{
		res += s[i] - '0';
	}
	cout << res << endl;
    return 0;
}

man!

题面

思路

        很基础的bfs,也是从这里开始上算法。我们暴力扫一遍各个位置,并且用一个st二维数组标记以及被计算过的位置,然后对所有没有被计算过的1位置跑bfs把所有与之联通的1放在一个集合里当作一个连通块就能知道面积。最后再通过计算各个连通块里有多少与0相邻的1或者在边界的1就能计算出周长。然后最后以pair的形式存在数组里排个序输出就行。

代码

#include<bits/stdc++.h>
using namespace std;
int dx[4] = {1,0,-1,0};
int dy[4] = {0,-1,0,1};
int main()
{
	int n,m;
	cin >> n >> m;
	vector <vector <char>> g(n+1,vector<char>(m+1));
	vector <vector <bool>> st(n+1,vector <bool>(m+1,false));
	
	for(int i = 1; i <= n; i ++)
	for(int j = 1; j <= m; j ++)
	cin >> g[i][j];
	
	vector <pair<int,int>> res;
	vector <vector <pair<int,int>>> num;
	for(int i = 1; i <= n; i ++)
	{
		for(int j = 1; j <= m; j ++)
		{
			if(st[i][j] || g[i][j] == '0') continue;
			else 
			{
				vector <pair<int,int>> tmp;
				queue <pair<int,int>> p;
				p.push({i,j});
				int sum = 0;
				while(p.size())
				{
					auto t= p.front();p.pop();
					if(st[t.first][t.second]) continue;
					else st[t.first][t.second] = true;
					tmp.push_back({t.first,t.second});
					for(int k = 0; k < 4; k ++)
					{
						int x = dx[k] + t.first;
						int y = dy[k] + t.second;
						if(x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] == '1' && st[x][y] == 0)
						{
							p.push({x,y});
						}
					}
				}
				num.push_back(tmp);
			}
		}
	}
	for(vector <pair<int,int>> t: num)
	{
		int sa = 0;
		int sb = t.size();
		for(auto tt: t)
		{
			for(int i = 0; i < 4;  i ++)
			{
				int x = tt.first + dx[i];
				int y = tt.second + dy[i];
				if(x < 1 || x > n || y < 1 || y > m || g[x][y] == '0'){sa ++;break;}
			}
		}
		res.push_back({sb,sa});
	}
	sort(res.begin(),res.end());
	for(auto [x,y] : res) cout << x << " " << y << endl;
	return 0;
}

mid部分

洞穴逃生

题面

思路

这道题的X数组就是为了求val数组的, 所以按照题意把val数组求出来之后就可以把X数组扔掉.

随后我们考虑题意,首先我们要求最大值所以多走一格就是赚到,所以最优解一定是把所有格子全部走完,并且一个格子只有第一次走到才会产生贡献,所以每个格子只会从它旁边最优的格子走过来,其他不是最优的格子就不会对这个格子产生影响,把这种影响抽象成边的话,我们不难发现,这种影响组成了一棵树.此时易得这是一个图论问题.

既然是图论问题,我们就需要考虑如何建图,每个格子都有可能和他相邻的格子产生贡献,所以我们对两个相邻的格子(假设对格子(i,j)和格子(i+1,j))建一条边,边的值就是V_{i,j}\times V_{i+1,j}, 这样我们就把图建好了.

其次考虑用什么图论算法, 我们每一个格子都需要贪心的选择最优的解法也就是最大的边,这种思想与最小生成树相似,但是求得是最大值,那么我们直接对反图求最小生成树即可求解.( 这里的反图就是所有边取相反数, 也就是-(V_{i,j} \times V_{i+1,j})

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
int p[250010],x[250010];
pair<int,pair<int,int>>k[250010];
int find(int x){
    if(p[x]!=x)p[x]=find(p[x]);
    return p[x];
}
signed main(){
    int t;
    cin>>t;
    while(t--){
        int n,m,sr,sc,tr,tc;
        cin>>n>>m>>sr>>sc>>tr>>tc;
        int x1,x2,a,b,c,pp;
        cin>>x1>>x2>>a>>b>>c>>pp;
        x[1]=x1,x[2]=x2;
        int cnt=0,ans=0;
        for(int i=1;i<=n*m;i++)p[i]=i;
        for(int i=3;i<=n*m;i++)x[i]=(a*x[i-1]+b*x[i-2]+c)%pp;
        for(int i=1;i<=n*m;i++){
            if(i%m)k[cnt++]={-x[i]*x[i+1],{i,i+1}};
            if(i<=(n-1)*m)k[cnt++]={-x[i]*x[i+m],{i,i+m}};
        }
        sort(k,k+cnt);
        for(int i=0;i<cnt;i++){
            int u=k[i].second.first,v=k[i].second.second;
            int uu=find(u),vv=find(v);
            if(uu!=vv){
                p[uu]=vv;
                ans+=k[i].first;
            }
        }
        cout<<-ans<<endl;
    }
}

学长的圣遗物

题面

思路

        这题考虑二分答案,既选k个物品总价值与总重量的比值

        价值 x = b_i / a_i​ 等式两边同时乘a_i​可以得到x\cdot a_i = b_i

        我们要所选的k个总单位价值最大,即要选的数量中b_i - a_i * x 最大,所以二分的check函数对b_i - a_i \cdot x 按降序排列,算出前k个之和是否大于0,大于0就说明当前单位价值可以到达,反之不能。

代码

#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);

const int INF = 0x3f3f3f3f;
const int MAX = 1e5 + 7;
int n, k;
int a[MAX], b[MAX];
double w[MAX];

bool judge(double mid);
 
int main() {
    IOS;
    int T; cin >> T;
    while (T--) {
        cin >> n >> k;
        for (int i = 1; i <= n; i++) cin >> a[i] >> b[i];
        double l = 0, r = INF;
        for (int i = 1; i <= 100; i++){ 
            double mid = (l + r) / 2;
            if (judge(mid)) l = mid;
            else r = mid;
        }
        cout << fixed << setprecision(2) << l << endl;
    }
    return 0;
}
bool judge(double x) {
    for (int i = 1; i <= n; i++)
        w[i] = b[i] - a[i] * x;
    sort(w + 1, w + 1 + n, greater<double>());
    double sm = 0;
    for (int i = 1; i <= k; i++)
        sm += w[i];
    return sm > 0;
}

mid hard部分

一个简单数学题(easy version)

题面

思路

用一个大小为 5 的数组 dp 来维护当前可达到的“和模 5”的子集个数,dp[r] 表示子集和 ≡ r (mod 5) 的子集数。

初始时只有空集,其和为 0,因此 dp[0] = 1,其余为 0。

依次枚举每个数 i∈[1..N],对 dp 进行更新。对每个余数 r,既可以“不选 i”,也可以“选 i”。“选 i”会将原来和为 r 的那些子集的和变成 (r + i) mod 5

easy version的本题n的数据量只有1e6级别,可以用dp解决。

代码

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MOD = 1000000007;

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    cin >> N;
    vector<int> dp(5, 0LL);
    dp[0] = 1;  

    for(int i = 1; i <= N; i++){
        int x = i % 5;
        int new_dp[5];
        
        for(int r = 0; r < 5; r ++){
            int sf = dp[r];
            int fs = dp[(r + 5 - x) % 5]; 
            new_dp[r] = (sf + fs) % MOD;
        }
        
        for(int r = 0; r < 5; r++){
            dp[r] = new_dp[r];
        }
    }

    cout << dp[0] % MOD << endl;
    
    return 0;
}

选择排序

题面

思路

• 设第一个(或第二个)操作应用的范围为 [1, x](或 [y, n])。
• 猜测 y。
• 如果 [1, x] 和 [y, n] 不相交,找到离 y 最远的 x,使得范围 [x, y] 内的元素处于正确的位置。
• 否则,如果 [1, x] 和 [y, n] 相交,找到最小的 x,使得如果你对前 x 个元素进行排序,那么前 y - 1 个元素会处于正确的位置。
• 上述方法可以在 O(n) 时间内实现。一个 O(n log n) 时间复杂度的算法也可以在时间限制内通过所有测试用例。

代码

#define IOS ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define PII pair<int,int>
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
void solve(){
	cin >> n;
	vector <int> a(n + 1);
	for(int i=1;i<=n;i++)
	{
		cin >>a[i];
	}
	int maxn = 0;
	vector <int> b=a;
	sort(b.begin() + 1,b.end());
	vector <int> p(n + 1);
	map <int,int> st;
	for(int i=1;i<=n;i++)
	{
		int k = lower_bound(b.begin()+1,b.end(),a[i])-b.begin();
		if(st[k])
		{
			p[i] = ++st[k];
		}
		else st[k] = k,p[i]=k;
	}
	vector <int> tmp;
	int res=n * n;
	int minn =1e18;
	for(int i=n;i>=1;i--)
	{
		if(p[i] < minn && p[i] < (i)) {minn = p[i];tmp.push_back(p[i]);}
	}
	for(int i=1;i<=n;i++)
	{
		int sk;
		if(tmp.size())
		sk = tmp.back();
		else sk = n + 1;
		sk = n - sk + 1;
		sk = min(sk,n - i + 1);
		int k = maxn * maxn + sk * sk;
		res = min(res,k);
		if(p[i] > i && p[i] > maxn)
		{
			maxn = max(maxn,p[i]);
		}
		if(tmp.size() == 0) continue;
		if(p[i] == tmp.back()) tmp.pop_back();
	}	
	minn = n + 1;
	tmp.clear();
	maxn = 0;
	for(int i=1;i<=n;i++)
	{
		if(p[i] > i && p[i] > maxn)
		{
			maxn=p[i];
			tmp.push_back(p[i]);
		}
	}
	for(int i=n;i>=1;i--)
	{
		int sk;
		if(tmp.size())
		{
			sk = tmp.back();
		}
		else {
			sk = 0;
		}
		sk = min(sk,i);
		res = min(res,(n - minn + 1) * (n - minn + 1) + sk * sk);
		if(p[i] < i && p[i] < minn)
		{
			minn = p[i];
		}
		if(tmp.size() == 0) continue;
		if(p[i] == tmp.back()) tmp.pop_back();
	}
	cout << res << endl;
}
signed main()
{
    IOS;
    solve();
}

省电

题面

思路

思路:区间dp

设 dp[i][j] 表示 [l,r] 区间人在左端点里最小的耗电值。

dp[r][l] 表示 [l,r] 区间人在右端点的最小值。

设起点为st, 因为 l \le st, r \ge st 所以这样设置dp不会有重叠的情况。

然后推方程:先用前缀和记录前i个电灯的每秒耗电量,同时可以注意到人在左端点的时候,可以是上一个左端点向左边走一步,也可以是右边的右端点走到了这个区间的左端点。那么方程就是:

dp[l][r]=min(dp[l][r],dp[l+1][r]+(a[l+1]-a[l])\times(sum[n]-sum[r]+sum[l]),dp[r][l+1]+(a[r]-a[l])\times(sum[n]-sum[r]+sum[l]));

其中sum[n]-sum[r]+sum[l] 表示区间 [l+1,r] 的每秒耗电量。

同理

dp[r][l]=min(dp[r][l],dp[r-1][l]+(a[r]-a[r-1])\times(sum[n]-sum[r-1]+sum[l-1]),dp[l][r-1]+(a[r]-a[l])\times(sum[n]-sum[r-1]+sum[l-1]));

方程推出来了那我们就直接枚举起点左边的l和起点右边的r就好啦,最后选区间为 [1,n] 左端点或者右端点最小的输出

代码

#include <string.h>
#define int long long
using namespace std;
int a[1010];
int dp[1010][1010];
int b[1010];
int sum[1010];
signed main()
{
    int n;
    int t;cin>>t;
    while(t--){   
        cin>>n;
        for(int i = 0; i <= n; i ++) for(int j = 0; j <= n;  j++) dp[i][j] = 1e18;
        int st;
        cin>>st;
        for(int i=1;i<=n;i++){
            cin>>a[i]>>b[i];
            sum[i]=sum[i-1]+b[i];
        }
        dp[st][st]=0;
        for(int l=st;l>=1;l--){
            for(int r=st;r<=n;r++){
                    int tot=sum[n]-sum[r]+sum[l];
                dp[l][r]=min(dp[l][r],min(dp[l+1][r]+(a[l+1]-a[l])*tot,dp[r][l+1]+(a[r]-a[l])*tot));
                dp[r][l]=min(dp[r][l],min(dp[r-1][l]+(a[r]-a[r-1])*(sum[n]-sum[r-1]+sum[l-1]),dp[l][r-1]+(a[r]-a[l])*(sum[n]-sum[r-1]+sum[l-1])));
            }
        }
        cout<<min(dp[1][n],dp[n][1])<<endl;
    }
    return 0;
}

hard部分

唐山皇帝

题面

思路

首先将题目进行一定的转化,注意到对于一个 妖怪(数字)a_i,我们可以执行(a_i \ mod \ k) + mk 次操作来使其符合条件(n 为非负整数)。当确定了每个妖怪需要被执行的操作次数后,我们便可以容易地得出答案。

假设确定了每个妖怪需要被执行的操作次数为 [1,2,3,0,2,4],如下图所示,总共需要执行的操作次数即为红线标出的总长度,即 1+1+1+2+2=7。

不妨将每个妖怪的取值看作高度,将一个妖怪需要被操作的次数加上 k 称作提升。同时,由题意,高度恰好等于 k 的妖怪可以视为高度为 0,在下文中默认将所有高度恰好为 k 的妖怪的高度看作 0。

假设已经确定了 i 个妖怪需要被操作的次数,考虑第i+1个妖怪,将第 i 个妖怪的高度记作 h_i

1.若 h_{i+1} \le h_i,那么可以直接不做任何提升操作。

2.若h_{i+1} > h_i,有以下两种选择:

           \circ   不做任何抬升操作,将答案增加h_{i+1}-h_i;

           \circ   选择一个j \le i,使得 h_j + k - h_{j-1} - max(0,h_j - h_{j-1}) 最小(这个最小值后文记作\Delta i),并提升[j,i] 范围中的每个妖怪,将答案增加\Delta i

第一种操作很好理解,第二种操作的思想是将新增的妖怪前的妖怪提升,使得新增的妖怪不对答案产生贡献。同时,容易得知,如果选定一个妖怪,并将其及其后所有妖怪提升,其后妖怪产生的贡献不变,只有被选定的妖怪贡献产生变化。可参考下图。

在两种操作中选择一个贡献较小的,就完成了第i+1个妖怪的计算。

这里按照这个思路直接模拟两种操作即可,不提供参考代码。

然而,每新增一个妖怪,这种方法就要向前遍历每一个妖怪,时间复杂度为O(n^2),无法通过此题。

优先队列优化:

注意到,在上面的方法中,每个 j 最多被选中一次,且选择任意一个 j 进行操作后都不会影响其他任意一个 \Delta i​。那么,在每次新增一个妖怪后,我们可以预先计算好这个妖怪的 \Delta i,并将其压入一个优先队列,这样之后的查询可以以 O(1)  的复杂度完成。同时,由于每个 j 最多被选中一次,当我们通过优先队列获取到对应的 j 后,只需要将其出队而不需重新计算并入队。

引入优先队列进行优化后,每个妖怪需要花费O(log\ n) 的时间计算 \Delta i 并入队,另外还可能需要 O(log\ n) 的时间出队。总体时间复杂度为 O(log\ n) ,使用的额外空间主要来自优先队列,空间复杂度 O(n)

代码

#include<bits/stdc++.h>
using namespace std;
int a[200009];
void s()
{
	int n,k;
	cin>>n>>k;
	for(int i=1; i<=n; i++)
	{
		cin>>a[i];
		if(a[i]==k)a[i]=0;
	}
	long long ans=0;
	priority_queue<long long , vector<long long> , greater<long long> >que;//小根堆
	for(int i=1; i<=n; i++)
	{
		//上升 a[i]-a[i-1];
		int cha=a[i]-a[i-1];
		if(!que.empty()&&que.top()+k<cha)//可以贪
		{
			ans+=(que.top()+k);
			que.pop();
			que.push(cha-k);//将改变后的下降高度继续丢入小根堆
		}
		else
		{
			if(cha<0)que.push(cha);
			ans+=max(0,cha);//只加上升量
		}

	}
	cout<<ans<<endl;
}

int main()
{
	int t;
	cin>>t;
	while(t--)s();
}

very hard(防ak)部分

一个简单数学题(hard version)

改编自IMO数学竞赛的练习中的数学题,经过研一学长的验题,本题目被他ac了以后从最难题的地位降到了第二。题解主要讲生成函数结合复变函数的解题思路。

一开始本题目不允许使用矩阵快速幂,后面心软了发布E题能用矩阵的公告。

如果采取矩阵快速幂的方式本体难度直线下降到mid hard。如果通过推论的方式本题的难度是非常高的。矩阵快速幂的题解在此不做赘述,此题解证明结论的严格推论。

本题有两人是用结论通过此题(金鳞玲同学以及李武同学)tql。

题面

思路

强行解释太生硬,让我们从一个具体的例子来展开。比如样例的N = 5 这种情况。我们知道数字集合为 \{1,2,3,4,5\}。那么它的子集根据总和可以分为下列图中几类:

总和为0、5、10、15的子集加在一起总共有8个。因此本例答案为8。但是我显然不是为了让你数数而告诉你这些。我们换另一种角度看看能不能得到答案。

我们知道集合中的每个元素都有两种选择,选或不选,所以我们一开始就知道了原集合的所有子集的数量是2^N。这很像二项分布的样子对吗,但是本体总和的计算是深受数值的影响的,每个元素的权重不同,不能直接通过二项分布直接得出。

但是这个转化很简单,我们知道 (1+x)^N 是二项分布的形式,在样例中不妨先直接套用令p(x) = (1+x)(1+x)(1+x)(1+x)(1+x) ,然后我们套上权值就得到p(x) = (1+x)(1+x^2)(1+x^3)(1+x^4)(1+x^5)

这里解释为什么这么做。在二项分布也就是第一个式子中 (1+x) 表示了一个数的两种选择,如果当前这个数字出现在我想要的子集里那么我就从1和x中选择x,否则选择1。而第二个式子中(1+x^i) 中i表示当前的数字是几。

因此当我们把p(x) = (1+x)(1+x^2)(1+x^3)(1+x^4)(1+x^5)展开后就如下图:

对应之前我们列出来的各种情况,x的次数对应总和,例如总和为8的子集个数我们就可以通过查找x^8的系数,对应上面的展开式也就是3。x^8可能是由x^3 \cdot x^5得到的也有可能是x^1\cdot x^3 \cdot x^4得到的诸如此类,所有的情况之和就是x^8的系数。换句话说就是总和为8的集合可能是\{3,5\} or \{1,3,4\}等等,但是总共有3个这样的集合。所以为了得到本题目的答案,我们只需要把x^0,x^5,x^{10},x^{15}的系数相加就好,也就是 1 + 3 + 3 + 1 = 8 。

然后我们把他推广到N

这样就是f(x) = (1+x)(1+x^2) ... (1+x^n)

这里我们再通过另一个我们已经知晓的东西来引进后面的内容。我们将推广后的式子展开成f(x) = c_0 + c_1x + c_2x^2 + ... + c_Nx^N

然后将 x = 1 代入 f(x) = (1+x)(1+x^2) ... (1+x^n)  易得f(1) = (1 + 1) \times (1 + 1) ...= 2^N = c_0 + c_1 + c_2 + ... c_N

同理f(-1) = 0 = c_0 - c_1 + c_2 ... - c_N (N \ mod \ 5 = 0)

所以f(1) + f(-1) = 2\cdot c_0 + 2 \cdot c_2 + .... + 2 \cdot c_{N-1} = 2^N

为什么只有偶数项了呢?因为(-1)的周期是2。如果题目要求的是元素和为2的倍数的子集个数就好了。周期是2的我能推,那么周期为5的我是不是也能推呢?

聪明人已经发现可以用复数解决这样的问题。我们令\zeta ^ 5 = 1。在复平面上的\zeta:

类比f(1),f(-1),我们要计算的是

展开来如图:

类似向量运算我们很快就知道\sum_{i=0}^{4}\zeta ^ i = 0。细心观察发现上面的图片中只有第5的倍数项是不为0的(自己画图就知道),而且知道是5。

所以:

也就是说我们只需要算出右边的式子就能知道答案了。

我们从\zeta 的5周期性很容易就能推出f(\zeta^0) = f(1)=2 ^ N, f(\zeta^1) =f(\zeta^2) = f(\zeta^3) = f(\zeta^4)

所以我们只需要知道f(\zeta) 的值就行了。

再根据\zeta的5周期性得:

f(\zeta) = ((1+\zeta)(1+\zeta^2)(1+\zeta^3)(1+\zeta^4)(1+\zeta^5))^{N/5}

为了计算(1+\zeta)(1+\zeta^2)(1+\zeta^3)(1+\zeta^4)(1+\zeta^5)

我们设g(x) = (x^5 - 1)

将其因式分解:g(x) = (x - \zeta)(x - \zeta^2)(x-\zeta^3)(x-\zeta^4)(x-\zeta^5)

将 -1 带入得知g(-1) = (-1)^5 - 1 = (- 1 -\zeta)(-1 -\zeta^2)(-1-\zeta^3)(-1-\zeta^4)(-1-\zeta^5)

所以:(1+\zeta)(1+\zeta^2)(1+\zeta^3)(1+\zeta^4)(1+\zeta^5) = 2

本题的答案就是\frac{1}{5} \cdot (2^N + 4 \cdot 2^{N/5})

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 1e9+7;
int qmi(int a, int b)
{
	int res = 1;
	while(b)
	{
		if(b&1)res = res * a % mod;
		a = a * a % mod;

		b >>= 1;
	}
	return res;
}
void solve()
{
	int n;
	cin >> n;
	int k = qmi(2,n);
	int kk = qmi(2,n/5);
	int sk = k + kk * 4;
	sk %= mod;
	sk *= qmi(5,mod - 2);
	sk %= mod;
	cout << sk << endl;
}

signed main()
{
	int t=1;while(t--)solve();
}

小孙无法思考

题面
 

思路

  • 提示 #1

    考虑如何确定是否可以生成 01 字符串 t 。

  • 提示 #2

    尝试设计一个只需要记录有用变量的 DP。

  • 提示 #3

    使用数据结构优化,或优化转移复杂度。

假设对 01 串 s 执行若干次操作后得到 01 串 t,不难发现 t 中每个元素一定对应 s 中一个子集的元素的 max。进一步观察可以发现这个子集一定构成一段区间,不妨写下 ti​ 对应 k=li​maxri​​sk​。

初始的串 t=s,因而所有 li​=ri​=i。假设当前的串 t 长度为 m,对应 l,r 两个序列,考虑如果在 1≤p≤m 的位置 p 执行了一次操作,得到的新序列 t′ 对应的 l′,r′ 两个序列。那么由于 ∀1≤i<p,ti′​=max(ti​,ti+1​),∀p≤i<m,ti′​=ti+1​,可以发现 ∀1≤i<p,li′​=li​,ri′​=ri+1​,∀p≤i<m,li′​=li+1​,ri′​=ri+1​。如果只关注 l,r 两个序列到 l′,r′ 两个序列的变化,相当于删去了 lp​ 和 r1​ 两个值。

因此从 s 序列出发做 k 次操作,能生成的序列 t 对应的 l,r 序列,一定 l 序列是从 1∼n 任意删去 k 个值,r 序列是 k+1∼n。

现在,不妨考虑如何判定 01 串 t 能否被生成,将 t 反转得到 t′ 后,相当于目标是寻找 n≥p1​>p2​>...>pk​≥1 使得 ∀1≤i≤k,ti′​=k=pi​maxn−i+1​sk​。一个显然正确的贪心策略是按照 i=1∼k 的顺序选择 pi​,每次都选择最大的可行值。

现在考虑进行 DP,设 dpi,j​ 为有多少长度为 i 的 01 串 t,使得运行上述贪心算法后 pi​ 恰好取到 j。可以认为必定 p0​=n+1,边界情况是 dp0,n+1​=1。考虑从 dpi−1,j​→dpi,∗​ 的转移:

  • 如果 s[j−1,n−i+1] 已经出现了 1,那么 t 反转后的第 i 位必须为 1,且必然 pi​=j−1 ,将 dpi,j−1​ 加上 dpi−1,j​。
  • 如果 s[j−1,n−i+1] 未出现 1,t 反转后的第 i 位可以为 0,如果为 0 必然取 pi​=j−1,还是将 dpi,j−1​ 加上 dpi−1,j​;如果希望 t 反转后的第 i 位为 1,需要找到最大的 pos≤n−i+1 使得 spos​ 为 1,然后取 pi​=pos,将 dpi,pos​ 加上 dpi−1,j​。

两类转移可以看作是无论如何将 dpi,j−1​ 加上 dpi−1,j​。然后找到最大的 pos≤n−i+1 使得 spos​ 为 1,对所有 j−1>pos 即 j≥pos+2 的 dpi,pos​ 加上 dpi,j​。

可以将第一种转移视作对 DP 数组进行了整体平移,然后第二种转移是求 DP 数组一段后缀和再执行一次单点加,记录偏移量后容易使用线段树在 O(nlogn) 执行所有转移。

答案即是所有 1≤i≤n,1≤j≤n 的 dpi,j​ 之和,使用线段树维护时当然也可以 O(1) 求出当前的 dpi​ 的每一项之和(需要将 dpi−1,1​ 这一项平移后下标不在范围内的置零)。

由于转移有着更好的性质,实际上可以不用数据结构仅仅更巧妙地使用前缀和 O(n) 解决该问题,但这不是必要的。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=998244353;
const int N=1e6+5;
const int iu=1e6;
int n;
ll bit[N];
void upd(int id,ll v){
	for(int i=id; i<=n ;i+=i&-i) bit[i]=(bit[i]+v)%mod;
}
ll qry(int id){
	ll res=0;
	for(int i=id; i>=1 ;i-=i&-i) res+=bit[i];
	return res%mod;
}
char c[N];
int nxt[N];
void solve(){
	string s;cin >> s;n=s.size();
	for(int i=1; i<=n ;i++) c[i]=s[n-i];
	for(int i=1; i<=n ;i++) bit[i]=0;
	upd(1,1);
	nxt[n+1]=n+1;
	for(int i=n; i>=1 ;i--){
		nxt[i]=nxt[i+1];
		if(c[i]=='1') nxt[i]=i;
	}
	ll ans=0;
	for(int i=1; i<=n ;i++){
		if(c[i]=='0' && nxt[i]!=n+1){
			int frog=nxt[i]-i+1;
			ll add=qry(frog-1);
			upd(frog,add);
		}
		ans+=qry(n-i+1);
	}
	ans%=mod;
	cout << ans << '\n';
}
int main(){
	ios::sync_with_stdio(false);cin.tie(0);
	int t;cin >> t;
	while(t--){
		solve();
	}
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值