Bootstrap

惠州学院第二届大学生程序设计竞赛c++题解

题目总览

按难度等级排序

题目难度知识点
7-1 关于闭区间的最值问题签到二分答案 / 数学
7-4 Nomi的计算器简单栈+模拟
7-9 玛恩纳简单模拟
7-10 涂矩阵简单前缀和+差分
7-7 求和游戏中等贪心
7-2 大厂必备技能!!!中等最短路径
7-8 得分王中等深搜+广搜 / 全排列+拓扑
7-5 篮球二叉树稍难思维+构造
7-3 这可能是道签到题稍难欧拉函数+矩阵+快速幂+等比数列
7-6 回文文回线段树+字符串哈希

7-1 关于闭区间的最值问题

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
给定 T 组测试样例
每组样例给定整数 n,x,请输出区间 [1, n] 中能被 x 整除的最大整数。

【输入格式】
第一行输入一个整数T,代表一共有T组测试样例。(1 ≤ T ≤ 1000)
接下来 T 行,每行两个整数 n, x。(1 ≤ x ≤ n ≤ 109 )

【输出格式】
对每一组输入,在一行中输出答案值。

【样例输入】

5
10 6
6 6
4 2
9 1
7 5

【样例输出】

6
6
4
9
5

【方法一:二分搜索】
一种直观的方法,可以通过二分枚举倍率的方法找到符合条件的最大倍率,x乘于最大倍率就是答案,例如一组输入:
13 2
倍率可以选1、2、3、4、5、6、7、8、9、10
第一次选5,2*5=10,满足条件,那么缩短左边界
倍率可以选5、6、7、8、9、10
第二次选7,2*7=14,不满足条件,那么缩短右边界
倍率可以选5、6
第三次选6,2*6=12,满足条件,那么缩短左边界
倍率可以选6
最后确定最大的倍率是6,那么答案是6*2=12

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
int main(){    //主函数
    int t;
    cin>>t;
    while(t--){
        long long n,x;
        cin>>n>>x;
        long long l=1,r=1e9;    //左边界l,右边界r
        while(l<r){
            long long mi=(l+r)/2+1;    //取中位数mi
            if(mi*x<=n){    //满足条件,左边界缩小
                l=mi;
            }else{    //不满足条件,右边界缩小
                r=mi-1;
            }
        }
        cout<<l*x<<endl;    //打印答案
    }
}

【方法二:数学】
简单的数学推论,n/x向下取整后再乘于x就是答案,例如一组输入:
13 2
答案是13/2*2=12

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
int main(){    //主函数
    int t;
    cin>>t;
    while(t--){
        long long n,x;
        cin>>n>>x;
        cout<<n/x*x<<endl;
    }
}

7-2 大厂必备技能!!!

时间限制: 2.0s 内存限制:256.0MB

【问题描述】
某团员工的素质不一般,天刚朦朦亮就准备让 iKun 和 Dij 去送快餐了。聪明的他们将自己居住的城市绘成一张地图,已知地图上有 n 个编号不同的城市,他们目前处在编号为 1 的城市上,送餐的目的地是编号为 n 的城市。此外,这些路都是单向的,从城市 u 到 v 需要花费 c 的时间。为了更高效地将快餐送到顾客手中,他们决定走从城市 1 到城市 n 花费最少的路。他们担心堵车,所以不能只知道一条路线,于是邀请你顺便探讨一下最少花费的路径有多少条?

【输入格式】
输入第一行为两个空格隔开的数 n (1 ≤ n ≤ 2000), m (0 ≤ m ≤ n∗(n−1)), 表示这张地图里有多少个城市及有多少边的信息。

下面 m 行,每行三个数 u, v, c, 表示从 u 城市到 v 城市有道路相连且花费为 c (1≤ c ≤10)。(其中保证 u
≠v,1 ≤ u,v ≤n,样例可能存在重边)

【输出格式】
输出包括两个数,分别是最少花费和花费最少的路径的总数。保证花费最少的路径总数不超过 2 30

两个不同的最短路径方案要求:长度均为最短路且每条路径经过的点的编号序列不同。

若无法到达城市 n,则在一行中输出 “No answer”。

【样例输入1】

5 4
1 5 4
1 2 2
2 5 2
4 1 1

【样例输出1】

4 2

【样例输入2】

5 2
1 2 4
1 3 2

【样例输出2】

No answer

【解释】
需要使用最短路算法,这个算法有很多种,这里只介绍dijkstra算法的做法。
1、dijkstra算法核心还是贪心,每次都选择最优的点去更新下一个点,用样例解释一下

5 4
1 5 4
1 2 2
2 5 2
4 1 1

在这里插入图片描述

(1) 起初,有一个数组d[6]={999,0,999,999,999,999},999代表这个点不可到达,d[1]=0,代表点1与点1的距离是0

(2) 开始,可以从点1到达点2和点5,更新数组d,此时,d[6]={999,0,2,999,999,4},点1到点2的距离是2,到点5的距离是4

(3) 点1已经使用过了,所以不能再次使用(因为再用已经没有意义了,这个点已经不可能贡献出更好的走法),所以剩下的点中选择离点1最近的点再做相同的操作

(4) 最优的点是2,可以从点2到达点5,更新d数组,d[6]={999,0,2,999,999,4},同样地,点2后面就不能再被使用了

(5) 最优的点是5,不能从点5到达任何点,结束,那么所有点与点1的距离就是d[6]={999,0,2,999,999,4}所表示的值

那么如何记录最短路的方案数呢?
(1) 只需要在求最短的时候额外记录一下数据就可以了,例如以上样例,有一个数组,cnt[6]={0,1,0,0,0,0,0},cnt[i]的意思的走到点i最短路的数量是cnt[i]的值

(2)如果最短距离变小了,对应的点方案数置0,重新计算,如果距离不变,那么直接累加,计算的公式是:如果从点1到点5,那么cnt[5]+=cnt[1],当然,要判断需不需要置0

(3) 点1跑了之后,cnt[6]={0,1,1,0,0,1}

(4) 点2跑了之后,cnt[6]={0,1,1,0,0,2}

跑完之后,计算出,点1到点5最短距离是4,有两种方案

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
vector<vector<int>> v[4000010];    //邻接矩阵存图
int ma[2010][2010];    //邻接表去重。。。其实可以直接用邻接表做
int d[2010];    //点1到其它点地最短距离
long long cnt[2010];    //最短距离方案数
priority_queue<vector<int>,vector<vector<int>>,greater<vector<int>>> q;    //优先级队列
signed main(){
    ios::sync_with_stdio(0);    //10的6次方以上的数据用cin、cout需要加速,不然会超时
    cin.tie(0);
    cout.tie(0);
    int n,m;
    cin>>n>>m;
    for(int i=0;i<2010;i++){    //初始化,所有的都没有边
        for(int j=0;j<2010;j++){
            ma[i][j]=INT_MAX/10;
        }
    }
    
    for(int i=0;i<m;i++){
        int u,v,c;
        cin>>u>>v>>c;
        ma[u][v]=min(ma[u][v],c);    //去重,取最优值
    }
    
    for(int i=0;i<2010;i++){    //构建邻接表
        for(int j=0;j<2010;j++){
            if(ma[i][j]!=INT_MAX/10){
                v[i].push_back({j,ma[i][j]});
            }
        }
    }
    for(int i=0;i<2010;i++){    //初始化,点1到其它点初始距离是个比较大的值,代表不可达,方案数初始化为0
        d[i]=INT_MAX/10;
        cnt[i]=0;
    }
    cnt[1]=1;    //到点1默认方案数为1
    d[1]=0;    //点1和点1距离为0
    q.push({0,1});    //初始添加点1
    while(!q.empty()){    //dijkstra算法
        vector<int> vv=q.top();
        q.pop();
        for(vector<int> vvv:v[vv[1]]){
            if(d[vvv[0]]>vv[0]+vvv[1]){    //更加优秀的方案
                d[vvv[0]]=vv[0]+vvv[1];
                cnt[vvv[0]]=cnt[vv[1]];    //置0后再计算
                q.push({d[vvv[0]],vvv[0]});    //新点添加到比较队列
            }else if(d[vvv[0]]==vv[0]+vvv[1]){    //最短距离一样的方案
                cnt[vvv[0]]+=cnt[vv[1]];    //直接增加
            }
        }
    }
    if(d[n]==INT_MAX/10){    //如果是最大值,代表点1没办法到达点n
        cout<<"No answer"<<endl;
    }else{    //打印结果
        cout<<d[n]<<' '<<cnt[n]<<endl;
    }
} 

7-3 这可能是道签到题

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
Nomi 需要出一道数学题,但是考虑到TeX太难搞了,所以还是出简单点好了。目前已知:
在这里插入图片描述

Nomi 会给你两个他喜欢的整数 x 和 y ,告诉他 f(x,y) 的值是多少就可以了,就这么简单。

提示:
1、ϕ(n) 表示的是小于等于 n 和 n 互质的数的个数,比如说 ϕ(1)=1,ϕ(4)=2,ϕ(5)=4。
2、⌈n⌉ 表示的是对 n 向上取整。

【输入格式】
第一行,输入两个整数 t(1 ≤ t ≤ 105 ),表示后续输入 t 次询问。

后续 t 行,每行输入两个整数 x 和 y (0 ≤ x,y ≤ 109),表示每一次的询问。

【输出格式】
输出 t 行,每行一个整数表示 f(x,y)的结果 ,结果对109 +7 取模。

【样例输入】

2
2 3
1000 123456

【样例输出】

365
500400402

【解释】
这个题涉及的知识点如下
1、条件1,欧拉函数,分析数据量,只需要打出107内的欧拉表就可以了
2、条件2,值为1
3、条件3,矩阵乘法+快速幂,可以看下图理解:
在这里插入图片描述

那么可推导出公式:
在这里插入图片描述
由于y的数据范围是0 ≤ x,y ≤ 109,暴力算矩阵乘法会超时,需要使用快速幂

4、条件4,等比公式+除法取模+公比1特判,可以看下图理解:
在这里插入图片描述
可以看出很明显的一个等比数列,额外加上yy*f(x,0),另外注意除法取模+公比1特判就可以了

【代码】

#include<bits/stdc++.h>
#define int long long    //定义int为long long 类型,贪方便而已
using namespace std;
const int N = 1e7+10;
int mod = 1e9+7;
int phi[N]; // phi[i] 表示 i 的欧拉函数值
int p[N], cnt; // 统计素数
int vis[N];
void init(){
    phi[1] = 1; // 情况1,注意初始化! 
	for (int i = 2;i <= N;i ++) { // 线性筛 2~n 
		if (!vis[i]) { // 说明i是素数 
			phi[i] = i - 1; // 情况2
			p[cnt++] = i;
		}
		for (int j = 0;j < cnt;j ++) {
			if (i * p[j] > N) break;
			vis[i * p[j]] = 1;
			if (i % p[j] == 0) {
				phi[i * p[j]] = phi[i] * p[j]%mod; // 情况3
				break;
			}
			phi[i * p[j]] = phi[i] * (p[j] - 1)%mod; // 情况4 
		}
	} 
    
}

int x,y;
int matrFpow(int d){    //矩阵快速幂
    d--;
    vector<vector<int>> an={{1,0},{0,1}};
    vector<vector<int>> v={{x,y},{1,0}};
    vector<vector<int>> temp;    //中间矩阵,防止原矩阵在计算时被修改
    while(d){
        if(d&1){
            temp=an;
            an[0][0]=temp[0][0]*v[0][0]+temp[0][1]*v[1][0];
            an[0][0]%=mod;
            an[0][1]=temp[0][0]*v[0][1]+temp[0][1]*v[1][1];
            an[0][1]%=mod;
            an[1][0]=temp[1][0]*v[0][0]+temp[1][1]*v[1][0];
            an[1][0]%=mod;
            an[1][1]=temp[1][0]*v[0][1]+temp[1][1]*v[1][1];
            an[1][1]%=mod;
        }
        temp=v;
        v[0][0]=temp[0][0]*temp[0][0]+temp[0][1]*temp[1][0];
        v[0][0]%=mod;
        v[0][1]=temp[0][0]*temp[0][1]+temp[0][1]*temp[1][1];
        v[0][1]%=mod;
        v[1][0]=temp[1][0]*temp[0][0]+temp[1][1]*temp[1][0];
        v[1][0]%=mod;
        v[1][1]=temp[1][0]*temp[0][1]+temp[1][1]*temp[1][1];
        v[1][1]%=mod;
        d/=2;
    }
    return (an[0][0]+an[0][1])%mod;
}
int fpow(int val,int d){    //正常的快速幂
    int ans=1;
    while(d){
        if(d&1){
            ans*=val;
            ans%=mod;
        }
        d/=2;
        val*=val;
        val%=mod;
    }
    return ans;
}


int f(int i,int j){
    if(i>=0&&j==0){    //条件1,欧拉函数
        return phi[(i+100)/100];
    }else if(i==0&&j==1){    //条件2
        return f(0,0);
    }else if(i==0&&j>=2){    //条件3,矩阵快速幂
        return matrFpow(j);
    }else if(i>0&&j>=1){    //条件4,等比公式+除法取模+公比1特判
        int a1=x*matrFpow(y)%mod;
        int q=y;
        int n=j;
        int ext=fpow(y,n)*phi[i/100+1]%mod;
        if(q==1){    //公比为1的时候特判
            return a1*n+ext;
        }
        return (((a1*((1-fpow(q,n)+mod)%mod)%mod*fpow(((1-q+mod)%mod),mod-2)%mod))%mod+ext)%mod;
    }
}
signed main(){
    init();
    int t;
    cin>>t;
    while(t--){
        cin>>x>>y;
        cout<<f(x,y)<<endl;    //调用f(i,j)函数
    }
}

7-4 Nomi的计算器

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
Nomi 制作了一个计算器,打算给还在读十六年级的 Liukw 使用。但可惜的是,Nomi 在这方面显得不太聪明, 做出来的计算器保留了先乘除后加减和从左向右的二元计算特性,但是内置的计算方式却充满了bug。于是委托整天沉迷于测试工作的 Wugh 帮忙, Wugh 熟能生巧,三两下就判断出了计算器真正的计算方式。

计算方式如下:(等号左边是输入的表达式,右边是实际上的计算方式)

  • 符号 + :a + b=a ⊕ b,其中 ⊕ 符号为按位异或运算。

  • 符号 - :a - b = a + b

  • 符号 * :a ∗ b=b / a

  • 符号 / :a / b=a * b \sqrt{b} b

现在给定一个仅包含非负整数与 +,−,∗,/ 四种符号的表达式,计算并输出其结果。Nomi 不喜欢浮点数,所计算过程中的每一步只要存在浮点数,都只会保留整数部分。同时题目保证计算过程不会出现中间结果超出int范围或者发生除0的情况。

【输入格式】
第一行,一个算术表达式字符串 s (1 ≤ s.length ≤ 106)。

【输出格式】
输出一个整数,表示该计算器的计算结果。

【样例输入】

4-3+2*5/3

【样例输出】

5

【样例解释】
先计算 2∗5,真正的计算方式是 5/2=2,目前表达式为 4−3+2/3。

计算 2/3,真正的计算方式是 2 * 3 \sqrt{3} 3 =2,目前的表达式为 4−3+2。

计算 4−3,真正的计算方式是 4+3=7,目前的表达式为 7+2。

计算 7+2,真正的计算方式是 7⊕2=5,所以结果为 5。

【解释】
计算器的本质并没有改变,只是实际的计算方式改变了,第一遍先处理乘法和除法,第二遍再处理加法和减法即可,具体可看代码理解

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
int main(){    //主函数
	string s;
	cin>>s;
	vector<int> v;
    vector<char> vo;    //储存还没计算的符号
	int num=0;
	char c='+';    //上一个计算符号
	for(int i=0;i<s.size();i++){
		if(s[i]>='0'&&s[i]<='9'){    //如果是数字,转成真正的数值
			num*=10;
			num+=s[i]-'0';
		}
		if(s[i]=='+'||s[i]=='-'||s[i]=='*'||s[i]=='/'||i==s.size()-1){    //如果是符号,或者是最后一个元素,开始计算
			if(v.size()==0){    //如果容器还没有数字,直接添加
				v.push_back(num);
				c=s[i];    //记录下次计算使用的符号
				num=0;
				continue;
			}
			if(c=='+'){    //加法先不计算
                v.push_back(num);    //记录第二遍计算需要使用的数值
                vo.push_back('^');    //记录第二遍计算需要使用的符号
			}else if(c=='-'){    //减法先不计算
				v.push_back(num);    //记录第二遍计算需要使用的数值
                vo.push_back('+');    //记录第二遍计算需要使用的符号
			}else if(c=='*'){    //计算乘法
                v.back() = num / v.back();    //真正的计算方式
			}else if(c=='/'){    //计算除法
				v.back()*=(int)sqrt(num);    //真正的计算方式
			}
			c=s[i];    //下一次计算使用的符号
			num=0;    //下次使用的数值置0
		}
	}
	int ans=v[0];
	for(int i=1;i<v.size();i++){    //按顺序计算最终结果即可
		if(vo[i-1]=='^'){
            ans^=v[i];
        }else if(vo[i-1]=='+'){
            ans+=v[i];
        }
	}
	cout<<ans<<endl;
}

7-5 篮球二叉树

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
作为真 IKUN 的 Nomi 觉得二叉树的就像一堆中分堆在一起,于是开始研究如何构造一棵篮球二叉树来证明自己对 kun 的喜爱。Nomi 认为只有中分是不够的,还要有铁山靠重心和背带的特性。他觉得你也是真正的 IKUN,所有把构造的任务交给了你,所以把任务交给了你。现在需要你协助他构造这一棵篮球二叉树,使得满足以下所有特性:

  • 中分特性:这是一棵深度为 n (1 ≤ n ≤ 106) 的满二叉树。

  • 背带特性:所有节点的和为 m (1≤m≤106) 且每一个节点的值 ai (1 ≤ ai​ ≤ 106) 。

  • 铁山靠重心特性:对于树中所有的非叶子节点值为ax​ ,它的左子节点值为 al 、右子节点值为ar​ ,满足 ar = a l + ax

如果 Nomi 能成功构造输出该二叉树,请按照节点编号的给出任何一种满足情况的篮球二叉树前序遍历。如果无法构造则直接输出 -1。

【输入格式】
第一行给出两个整数 n 和 m (1 ≤ n,m ≤ 106),分别表示存在二叉树的深度为 n 与所有节点的和 m。

【输出格式】
输出一行。如果能够构造,请给出任意一种能够满足条件的 n 个整数,分别表示节点前序遍历的值ai (1 ≤ ai ≤ 106 )。如果无法构造则直接输出 -1。

【样例输入1】

3 32

【样例输出1】

2 1 1 2 3 10 13

【样例输入2】

4 11

【样例输出2】

-1

【样例输入3】

3 12

【样例输出3】

-1

【解释】
需要自己去思考如何最优地去构造这个二叉树,下面直接提供思路。

一棵深度为4且满足铁山靠重心特性的最小 (节点的总和最小) 满二叉树如下:
在这里插入图片描述
节点和为26,如果,m<26,那么篮球二叉树是没办法构成的,如果m=26,上面的树就是答案,如果,m=60,那么可以构成吗?可以的,看下图你就明白了
在这里插入图片描述
只需要在两个叶子节点补上(60-26)/2=17即可,类似的,像28、30、32、34、36、38这些,都可以用这个办法构造出来,那么奇数呢?39怎么构造?事实证明,只有深度为奇数,通过调整根节点,才可能出现奇数的情况。因为父节点+左子节点=右子节点,深度为偶数,永远不会出现奇数。

如果考虑的深度为3,最优的奇数构造方法:

在这里插入图片描述

最优的偶数构造方法:
在这里插入图片描述
可以看到,只要调整根节点的值为2,就可以切换奇偶性,这两种都是最优的构造方法,如果和不够,就可以通过调整两个叶子节点的值补满,如果和超出了,就表明不可构造。所以,只需要判断这两种方案是否可以构造出篮球二叉树就可以了

【代码】

#include<bits/stdc++.h>
using namespace std;
struct Tree{    //二叉树
    int val,l,r,d;
}t[2000010];
int idu=0;    //节点id
int sum=0;    //节点总和
int f=1;    //标记方案是否可行
int n,m;
void dfs(int id,int val,int d){    //构建总和最小、满足铁山靠重心特性的满二叉树,判断是否可以满足背带特性
    sum+=val;
    if(sum>m){
        f=0;
        return;
    }
    t[id].val=val;
    t[id].d=d;
    if(d+1<=n){    //高度不够,继续递归
        t[id].l=++idu;    //分配编号
        t[id].r=++idu;    //分配编号
        dfs(t[id].l,1,d+1);    //左子节点值为1
        dfs(t[id].r,val+1,d+1);    //右子节点值为当前节点值+1
    }
}
int flag;
void prin(int ind,int d){    //前序打印二叉树
    if(flag==1){
        cout<<' ';
    }
    flag=1;
    cout<<t[ind].val;
    
    if(d+1<=n){
        prin(t[ind].l,d+1);
        prin(t[ind].r,d+1);
    }
}
int main(){
    cin>>n>>m;
    if(n==1){    //高度为1,直接特判
        cout<<m<<endl;
        return 0;
    }
    if(n>20){    //高度超过20,一定不可以构成,因为每个节点的值至少为1,高度为20的满二叉树节点数超过了10的6次方
        cout<<-1<<endl;
        return 0;
    }
    dfs(0,1,1);
    if(f==1&&(m-sum)%2==0){    //根节点为1是否可以构建篮球二叉树
        t[(1<<n)-2].val+=(m-sum)/2;    //和不够,叶子节点补齐
        t[(1<<n)-3].val+=(m-sum)/2;    //和不够,叶子节点补齐
        prin(0,1);
        return 0;
    }
    f=1;    //初始化
    idu=0;
    sum=0;
    dfs(0,2,1);
    if(f==1&&(m-sum)%2==0){    //根节点为2是否可以构建篮球二叉树
        t[(1<<n)-2].val+=(m-sum)/2;    //和不够,叶子节点补齐
        t[(1<<n)-3].val+=(m-sum)/2;    //和不够,叶子节点补齐
        prin(0,1);
        return 0;
    }
    cout<<-1<<endl;    //都不可以,打印-1
    
}

7-6 回文文回

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
给一个仅由小写字母组成的字符串S,有q次操作。

操作有以下两种类型:

“1 x c” 将位置x的字符修改为字符c。

“2 L R” 询问 s[L : R] 是否是回文串,如果是,输出"Yes",否则输出"No"。

【输入格式】
第一行输入两个整数 n, q,代表字符串的长度和操作的次数。 (1 ≤ n ≤ 106 ,1 ≤ q ≤ 105)
第二行输入字符串 S,代表初始的字符串。(1 ≤ ∣S∣ ≤ 106)

接下来 q 行,每一行输入一个 op,代表操作的类型。(1 ≤ op ≤ 2)

如果 op 为1,则输入一个整数 x 和一个字符 c,表示将位于 x 处的字符修改为字符 c。(1 ≤ x ≤ n,c为小写字母)

如果 op 为2,则输入两个整数 L,R,代表查询的区间。(1 ≤ L ≤ R ≤ n)

【输出格式】
对于每次op为2的操作,在单独一行中输出 “Yes” 或 “No”。

【样例输入】

7 8
abcbacb
2 1 5
2 4 7
2 2 2
1 5 c
2 1 5
2 4 7
1 4 c
2 3 6

【样例输出】

Yes
No
Yes
No
Yes
Yes

【解释】

  首先将回文看成是正向的数字(hash),比如对ababacb在base为3的情况下,分别用1、2、3代表a、b、c,正序值为是1212132。然后我们将字符串逆序,也就是bcabcba,即逆序值为2312121。假如我们此时查询区间[1,5],等价于检查abcba是不是回文,在正序中有高五位为12121,逆序中有低五位为12121,12121=12121,证明hash的数值相同,即正反序代表的字符串是相同的(该区间就是回文)。

  由于看hash值的区段值,所以与一般的回文奇偶性是无关的,可以抛开以往回文的概念。那么由于区间问题,涉及到简单的单点修改,我们可以尝试用线段树去维护两棵线段树,分别维护整一个字符串的正序和反序的hash值,单点修改某一个数,区间查询字符串区间代表的hash值即可。

  由于hash碰撞,在字符串大与能够使用的最大数(unsigned long long最大值)的情况下,由于计算的溢出,可能出现相同的hash值代表了不同的字符串,所以只能说明答案正确的不一定正确,而答案错误的一定是错误的。

【代码】

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N = 1000005;
const ll base = 1e9+7;    //hash基础值,最好是质数
unsigned ll pf[N];
int n,m;
string s;
struct node{
	int l,r;
	unsigned ll v;
};
struct tree {    //线段树
    node tr[N<<2];
    
    void up(int p){    //懒加载
    	int mid=(tr[p].l+tr[p].r)>>1;
    	tr[p].v = tr[p<<1].v*pf[tr[p].r-mid]+tr[p<<1|1].v; 
	}
	
    void build(int p,int l,int r,bool rv){    //初始化
    	tr[p].l=l;
    	tr[p].r=r;
    	if(l==r){
    		if(!rv)tr[p].v=s[l]-'a'+1;
    		else tr[p].v=s[n-l+1]-'a'+1;
    		return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid,rv);
		build(p<<1|1,mid+1,r,rv);
		up(p);
	}
	
	void modify(int p,int l,int r,int x){    //修改
		if(tr[p].l==l&&tr[p].r==r){
    		tr[p].v=x;
    		return;
		}
		int mid=(tr[p].l+tr[p].r)>>1;
		if(l<=mid)modify(p<<1,l,r,x);
		else modify(p<<1|1,l,r,x);
		up(p);
	}
	
	unsigned ll query(int p,int l,int r){    //查询
		if(tr[p].r<l||tr[p].l>r){
			return 0;
		}
		if(l<=tr[p].l&&tr[p].r<=r){
    		return tr[p].v*pf[r-tr[p].r];
		}
		int mid=(tr[p].l+tr[p].r)>>1;
		unsigned ll r1=query(p<<1,l,r);
		unsigned ll r2=query(p<<1|1,l,r);
		return r1 + r2;
	}
	
}t1,t2;
int main(){
	pf[0]=1;
	for(int i=1;i<=1000001;i++){
		pf[i] = pf[i-1]*base;
	}
	cin>>n>>m;
	cin>>s;
	s="#"+s;
	t1.build(1,1,n,0);    //一棵正序的树
	t2.build(1,1,n,1);    //一棵反转后的树
	int w,x;
	string y;
	while(m--){
		cin>>w>>x;
		if(w==1){
			cin>>y;
			int p = y[0]-'a'+1;
			t1.modify(1,x,x,p);    //两颗树同时修改
			t2.modify(1,n-x+1,n-x+1,p);    //两颗树同时修改
		}else{
			cin>>w;
			if(t1.query(1,x,w)==t2.query(1,n-w+1,n-x+1)){    //判断正序和反转后的字符串hash值是否一致
				cout<<"Yes"<<endl;
			}
			else{
				cout<<"No"<<endl;
			}
		}
	}
} 

7-7 求和游戏

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
Wugh和 Liukw 在用卡牌玩游戏,Yisr在旁边观战。每一场游戏开始时,每张卡牌上有一个正整数a i​ ,他们将卡牌摊开正面朝上,游戏包括以下两个步骤:

  • 第一步,Wugh 可以用魔法将卡牌中至多 k 张卡牌从世界上消失。
  • 第二步,Liukw 可以用魔法将卡牌中至多 x 张卡牌的数乘以 -1。

Wugh 希望最大化剩余卡牌的和,Liukw 则希望最小化剩余卡牌的和。双方都采用最优的策略进行游戏,那么你能帮助 Yisr 提前预知游戏最后剩下的卡牌和是多少吗?

【输入格式】
第一行,输入一个正整数 t (1 ≤ t ≤ 105),表示游戏次数。

对于每一场游戏:

第一行,包含三个正整数 n, k , x (1 ≤ n ≤ 2∗105,1 ≤ k,x ≤ n),n 表示初始卡牌的个数,k 和 x 的含义与题目描述一致。

第二行,包含 n 个正整数 ai (1 ≤ ai ≤ 1000),表示每一张卡牌上面的数值。

题目保证,对于所有游戏,n 的总和不会超过 2∗10 5

【输出格式】
对于每个样例,输出一个整数,代表最终剩余卡牌的总和。

【样例输入】

8
1 1 1
1
4 1 1
3 1 2 4
6 6 3
1 4 3 2 5 6
6 6 1
3 7 3 3 32 15
8 5 3
5 5 3 3 3 2 9 9
10 6 4
1 8 2 9 3 3 4 5 3 200
2 2 1
4 3
2 1 2
1 3

【样例输出】

0
2
0
3
-5
-9
0
-1

【解释】
很容易看出来的贪心性质,由于卡牌上的数值只会是正数,那么Liukw一定会尽可能地选择数值最大、数量最多的卡牌乘以-1。实际上,剩余卡牌的和就取决Wugh的选择了,如果Wugh要拿走x张牌,那么他一定会拿走前x大的牌。

只要枚举Wugh拿走牌的数量就可以了。

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
int main(){
	int t;
	cin>>t;
	while(t--){
		int n,a,b;
		cin>>n>>a>>b;
		vector<int> v;    //卡牌数组
		for(int i=0;i<n;i++){
			int va;
			cin>>va;
			v.push_back(va);
		}
		sort(v.begin(),v.end());    //小到大排序
		deque<int> qa;    //去掉卡牌数组
		deque<int> qb;    //乘以负1数组
		
		int sum=0;    //最后剩余卡牌的和
		while(v.size()>0&&a>0){    //先尽可能去掉总和更多牌
			a--;
			qa.push_back(v.back());
			v.pop_back();
		}
		while(v.size()>0&&b>0){    //尽可能使总和更多的牌乘以-1
			b--;
			qb.push_back(v.back());
			sum-=v.back();    //乘以-1的牌就要减去
			v.pop_back();
		}
		
		for(int c:v){    //没使用的牌直接累加
			sum+=c;
		}
		int ans=sum;
		while(!qa.empty()){    //枚举让卡牌消失的数量
			qb.push_front(qa.back());    //a即将出队的卡牌,b需要将其乘以-1,因为a数组的里元素的值永远大于b的
			sum-=qa.back();
			b--;
			if(b<0){    //如果乘以-1的卡牌数量超出了限制,就需要去掉最小的那张
				sum+=2*qb.back();
				qb.pop_back();
				b++;
			}
			qa.pop_back();    //出队最小的卡牌,消失的卡牌数量-1
			ans=max(ans,sum);
		}
		cout<<ans<<endl;
	}
} 

7-8 得分王

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
一个 n 行 m 列的方格图,图中会出现"0",“#”,"S"三种字符:

  • "0"代表该位置能走
  • "#"代表该位置不能走
  • "S"代表初始位置

你初始站在位置在 S 上,初始分值为 1。

图中会有 k 个得分点,得分点只会出现在给定地图的 “0” 的位置上,且同一个位置上不会出现多于一个得分点。

请输出从初始位置出发,不限步数,能够得到的最大得分。

得分点定义:得分点是一次性的。经过得分点后,分值立刻发生变化,得分点立刻消失,再次经过该得分点所在位置时不对分数造成任何影响。即当你的当前坐标首次与得分点所在坐标重合时,你的分值就会立刻变化,再次经过该点时对分值不造成任何影响。

注意:过程中不可以越过任何一个得分点。

【输入格式】
第一行输入 n,m。表示该方格图有 n 行 m 列。(1 ≤ n,m ≤ 8)

接下来 n 个字符串表示方格图。

然后是一个整数 k ,代表一共有 k 个得分点。(0 ≤ k ≤ min(8,n∗m) )

接下来 k 行,每行 4 个整数 x,y,t,v。 (1 ≤ x ≤ n, 1 ≤ y ≤ m, 1 ≤ t ≤ 2, 1 ≤ v ≤ 50)

x,y 代表该得分点位于第 x 行第 y 列。

t 代表得分类型(1表示加法,2表示乘法)

v 代表分值。

【输出格式】
在一行中输出最大得分。

【样例输入1】

1 5
00S00
2
1 1 1 5
1 5 2 2

【样例输出1】

12

【样例输入2】

3 5
00S00
000##
00000
4
1 1 1 5
1 5 2 2
3 3 2 3
3 5 1 10

【样例输出2】

56

【样例说明】
样例2一种最优的走法是,先到 (1,1),再到 (3,3),然后到 (3,5),最后到 (1,5),得到的分数((1+5)∗3+10)∗2=56

【解释】
深搜+广搜,数据量比较小,暴力所有情况即可

【代码】

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,m;
string s[8];
ll v[8][8]={0};
ll d[8][8][2]={0};
int f[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
int a[72][72];
int idx[72],id=1,mod[72];
int px=0;
int x0=-1,yy0;
int dfs(int x,int y){
	if(x<0||x>=n||y<0||y>=m){
		return 0;
	}
	if(d[x][y][0]==2){
		if(v[x][y]==0){
			v[x][y]=id+px;
			idx[id+px]=d[x][y][1];
			a[id][id+px]=1;
			a[id+px][id]=1;
			mod[id+px]=2;
			px++;
		}
		else{
			a[id][v[x][y]]=1;
			a[v[x][y]][id]=1;
		}
		return 0;
	}
	if(v[x][y]!=0){
		return 0;
	}
	v[x][y]=id;
	ll res=d[x][y][1];
	for(int i=0;i<4;i++){
		int xx = x+f[i][0];
		int yy = y+f[i][1];
		if(xx>=0&&yy>=0&&xx<n&&yy<m){
			if(s[xx][yy]!='#'){
				res+=dfs(xx,yy);
			}
		}
	}
	return res;
}
bool vp[72];
void dfs2(int p){
	if(vp[p])return;
	vp[p]=1; 
	for(int i=0;i<id;i++){
		if(a[p][i]){
			dfs2(i);
		}
	}
}
ll ck(vector<int> &num){
	bool vis[72]={0};
	vis[v[x0][yy0]]=1;
	ll res=0;
	for(int i=0;i<num.size();i++){
		if(!vp[num[i]])continue;
		if(!vis[num[i]]){
			return 0;
		}
		if(mod[num[i]]==1){
			res+=idx[num[i]];
		}
		else{
			res*=idx[num[i]];
		}
		for(int j=0;j<id;j++){
			if(a[num[i]][j]){
				vis[j]=1;
			}
		}
	}
	return res;

}
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>s[i];
		for(int j=0;x0==-1&&j<m;j++){
			if(s[i][j]=='S'){
				x0=i;
				yy0=j;
			}
		}
	}
	int t;
	cin>>t;
	int x,y;
	for(int i=0;i<t;i++){
		cin>>x>>y;
		x--;
		y--;
		cin>>d[x][y][0]; 
		cin>>d[x][y][1];
	}
	d[x0][yy0][1]+=1;
	for(int i=0;i<n;i++){
		for(int j=0;j<m;j++){
			if(v[i][j]==0&&s[i][j]!='#'){
				px=1;
				mod[id]=1;
				idx[id]=dfs(i,j);
				id+=px;
			}
		}
	}
	vector<int> nums;
	dfs2(v[x0][yy0]);
	for(int i=1;i<id;i++){
		nums.push_back(i);
	}
	ll ans=1;
	do {
		ans=max(ans,ck(nums));
    } while (next_permutation(nums.begin(), nums.end())); 
	cout<<ans<<endl;
}

7-9 玛恩纳

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
茶余饭后,Nomi 像往常一样打开了粥,无意间看到有玛恩纳的池子,看着包里仅剩下的几张寻访凭证,无脑的随便抽了一下,没想到三发就把玛恩纳给搞到了。喜出望外的 Nomi 掏出了仓库仅剩下的作战记录把玛恩纳直接精二了,迫不及待地跑到7-18去试一下三技能强度。

在这里插入图片描述

Nomi 认识到玛恩纳的强大,没有任何敌人能够抵挡住玛恩纳的一次攻击,此次每个地图都将带上玛恩纳,成为MaBaoNan。

玛恩纳三技能的攻击范围(方向朝上)如下,一共覆盖了14格(包括他本体所在的位置也能够攻击),同时玛恩纳的攻击方向是可以90度旋转的(也就是所存在上下左右四个方向的放置方式)。图中红色的是玛恩纳本体的位置,玛恩纳本体在地图中不能够位于障碍物上,而且不能够超出地图的范围。目前已知地图的大小为 n*m (1 ≤ n∗m ≤106 ,0 ≤ i ≤ n,0 ≤ j ≤ m),地图中可能存在障碍物ai,j (ai,j = −1,障碍物不会受到攻击) 和血量为 ai,j (0 ≤ ai,j ≤ 109) 的敌人。由于玛恩纳导致游戏难度太低了,每个地图 Nomi 都只会将玛恩纳部署到合适的位置一次(保证绝对可以部署),使得他能够攻击到的敌人血量之和最大。

在这里插入图片描述

【输入格式】
第 一 行输入两个整数n,m (1 ≤ n∗m ≤ 106),n表示地图的行数,m表示地图的列数。

第二到 n+1 行,每行都存在 m 个整数 ai,j (−1 ≤ ai,j​ ≤ 109),ai,j = −1 时表示当前位置为障碍物,其他整数则表示当前位置敌人的血量。

【输出格式】
第一行输出一个大写字母表示玛恩纳的攻击方向 (向上输出‘U’,向下输出‘D’,向左输出‘L’,向右输出‘R’)。

第二行输出一个整数表示玛恩纳攻击的敌人最大血量之和。

第三行分别输出两个整数表示玛恩纳的坐标 i 和 j (0 ≤ i < n,0 ≤ j < m)。

注意:同时存在最大血量之和时,坐标 i 尽可能小。i 相同时,坐标 j 尽可能小。i,j 相同时且存在多个方向都满足时,方向优先级按上、下、左、右的优先顺序即可。

【样例输入】

6 10
-1 0 -1 0 -1 -1 0 0 0 -1
0 -1 -1 -1 -1 -1 0 -1 -1 0
0 -1 0 -1 0 -1 9999 -1 -1 0
0 -1 0 -1 0 -1 1 0 -1 -1
0 -1 88 -1 0 -1 0 0 -1 -1
2 -1 90 -1 1 -1 0 -1 -1 -1

【样例输出】

D
10001
2 4

【解释】
模拟题,根据题目意思暴力枚举每一个条件,取最优的答案即可。

1、一个比较好的实现方法,就是打方向表,这样看起来比较直观,容易排bug,不用写那么多if else语句,具体可以看代码实现

2、输出的结果有优先顺序,实现的时候注意枚举的顺序就行了

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
int n,m;
vector<vector<long long>> v;    //动态存地图
int fu[14][2]={    //向上的方向表,(0,0)代表玛恩纳的位置,其它13格是相对玛恩纳的位移
    {-3,0},
    {-2,-1},{-2,0},{-2,1},
    {-1,-2},{-1,-1},{-1,0},{-1,1},{-1,2},
    {0,-2},{0,-1},{0,0},{0,1},{0,2}
};
int fd[14][2]={    //向下的方向表
    {3,0},
    {2,-1},{2,0},{2,1},
    {1,-2},{1,-1},{1,0},{1,1},{1,2},
    {0,-2},{0,-1},{0,0},{0,1},{0,2}
};
int fl[14][2]={    //向左的方向表
    {-2,0},{-2,-1},
    {-1,0},{-1,-1},{-1,-2},
    {0,0},{0,-1},{0,-2},{0,-3},
    {1,0},{1,-1},{1,-2},
    {2,0},{2,-1},
};
int fr[14][2]={    //向右的方向表
    {-2,0},{-2,1},
    {-1,0},{-1,1},{-1,2},
    {0,0},{0,1},{0,2},{0,3},
    {1,0},{1,1},{1,2},
    {2,0},{2,1},
};
long long getU(int x,int y){    //获取向上方向造成的伤害
    long long sum=0;
    for(int i=0;i<14;i++){    //根据方向表找到所有能攻击到的位置
        int xx=x+fu[i][0];
        int yy=y+fu[i][1];
        if(xx>=0&&xx<n&&yy>=0&&yy<m&&v[xx][yy]!=-1){    //攻击的地方不能超出地图,并且不能是障碍物
            sum+=v[xx][yy];
        }
    }
    return sum;
}
long long getD(int x,int y){    //获取向下方向造成的伤害
    
    long long sum=0;
    for(int i=0;i<14;i++){    //根据方向表找到所有能攻击到的位置
        int xx=x+fd[i][0];
        int yy=y+fd[i][1];
        if(xx>=0&&xx<n&&yy>=0&&yy<m&&v[xx][yy]!=-1){    //攻击的地方不能超出地图,并且不能是障碍物
            sum+=v[xx][yy];
        }
    }
    return sum;
}
long long getL(int x,int y){    //获取向左方向造成的伤害
    
    long long sum=0;
    for(int i=0;i<14;i++){    //根据方向表找到所有能攻击到的位置
        int xx=x+fl[i][0];
        int yy=y+fl[i][1];
        if(xx>=0&&xx<n&&yy>=0&&yy<m&&v[xx][yy]!=-1){    //攻击的地方不能超出地图,并且不能是障碍物
            sum+=v[xx][yy];
        }
    }
    return sum;
}
long long getR(int x,int y){    //获取向右方向造成的伤害
    
    long long sum=0;
    for(int i=0;i<14;i++){    //根据方向表找到所有能攻击到的位置
        int xx=x+fr[i][0];
        int yy=y+fr[i][1];
        if(xx>=0&&xx<n&&yy>=0&&yy<m&&v[xx][yy]!=-1){    //攻击的地方不能超出地图,并且不能是障碍物
            sum+=v[xx][yy];
        }
    }
    return sum;
}
int main(){
    cin>>n>>m;
    char dir='\0';    //方向
    long long ansX=-1,ansY=1,ans=0;    //分别是坐标x、坐标y、总伤害
    for(int i=0;i<n;i++){    //存地图
        vector<long long> vv;
        for(int j=0;j<m;j++){
            long long a;
            cin>>a;
            vv.push_back(a);
        }
        v.push_back(vv);
    }
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            if(v[i][j]!=-1){    //不能放置在障碍物位置
                if(getU(i,j)>ans){    //注意枚举顺序,上下左右
                    dir='U';
                    ans=getU(i,j);
                    ansX=i;
                    ansY=j;
                }
                if(getD(i,j)>ans){
                    dir='D';
                    ans=getD(i,j);
                    ansX=i;
                    ansY=j;
                }
                if(getL(i,j)>ans){
                    dir='L';
                    ans=getL(i,j);
                    ansX=i;
                    ansY=j;
                }
                if(getR(i,j)>ans){
                    dir='R';
                    ans=getR(i,j);
                    ansX=i;
                    ansY=j;
                }
                
            }
        }
    }
    cout<<dir<<endl;    //打印结果
    cout<<ans<<endl;
    cout<<ansX<<' '<<ansY<<endl;
}

7-10 涂矩阵

时间限制: 1.0s 内存限制:256.0MB

【问题描述】
Liukw 给你一个 n × m 的矩阵,矩阵中存在 R、G、Y 三种颜色,你需要使矩阵满足以下条件:

给你一个 n × m 的矩阵,让你使矩阵满足以下条件:

1、从最上方开始若干行(最少一行)的格子全是红色(用 R 表示);

2、接下来若干行(至少一行)的格子全是绿色(用 G 表示);

3、剩下的行(至少一行)的格子全是黄色(用 Y 表示)。

现在给你一个 n 行 m 列的矩阵,且每个格子的颜色为红色、绿色、黄色之一,你可以对矩阵的每个格子进行修改颜色,每次修改可以将矩阵的任意一个格子改成你想要的颜色,现在希望将这个矩阵变成满足上述条件,问你最少需要多少次修改操作。

【输入格式】
第一行两个整数 n (3 ≤ n ≤ 1000), m (1 ≤ m ≤ 1000)

接下来 n 行,每行 m 个字符,矩阵的每一个格子是 R (红色),G (绿色),Y (黄色) 中的一个。

【输出格式】
输出一个整数,表示最少需要修改多少块。

【样例输入】

4 5
RGYRG
YGRRY
GRYYR
YGYRG

【样例输出】

13

【解释】
1、106 数据直接暴力枚举是行不通,仔细分析容易发现,可以压缩每一行的数据,例如RGYRGGGGG可以用2个红色、6个绿色、1个黄色来表示,即使再多的数据,也只用数值来表示。因为题目求的是需要修改的次数,那么,可以记录刷成红色需要7次、刷成绿色需要3次、刷成黄色需要8次

2、数据压缩后可以暴力枚举红色行的数量、绿色行的数量、黄色行的数量来计算所有情况需要修改的次数,取最小值即可

3、需要使用前缀和 + 差分的技巧在O(1)的时间复杂度内计算指定情况修改的次数

前缀和与差分算法:
  前缀和与差分算法主要是为了快速求出某个区间的和,例如有一个数组a[10]={0,1,2,3,4,5,6,7,8,9},我们需要求a[3]到a[7]的和,传统的办法是求a[3]+a[4]+a[5]+a[6]+a[7],但是这样求复杂度是O(n),如果我们建立了一个前缀数组S[9]={a[0],a[0]+a[1],a[0]+a[1]+a[2],a[0]+a[1]+a[2]+a[3],……,a[0]+a[1]+……+a[9]},那么我们要求a[3]到a[7]的和可以用差分思想S[7]-S[2]即可求出

【代码】

#include<bits/stdc++.h>    //万能头文件
using namespace std;    //命名空间
int main(){    //主函数
    int n,m;
    cin>>n>>m;
    int r[1010]={0};    //每一行刷成红色需要的修改次数,例如,r[2]的值代表第二行全刷成红色需要的修改次数
    int g[1010]={0};    //每一行刷成绿色需要的修改次数
    int y[1010]={0};    //每一行刷成黄色需要的修改次数
    for(int i=1;i<=n;i++){
        string s;
        cin>>s;
        for(int j=0;j<s.size();j++){    //记录每一行刷成不同颜色各需要的修改次数
            if(s[j]!='R'){
                r[i]++;
            }
            if(s[j]!='G'){
                g[i]++;
            }
            if(s[j]!='Y'){
                y[i]++;
            }
        }
        r[i]+=r[i-1];    //做前缀和操作
        g[i]+=g[i-1];
        y[i]+=y[i-1];
    }
    int ans=INT_MAX;
    for(int i=1;i<=n-2;i++){    //1值i行刷成红色
        for(int j=i+1;j<=n-1;j++){    //i+1至j行刷成绿色,剩下j+1至n行刷成黄色
            ans=min(ans,r[i]+g[j]-g[i]+y[n]-y[j]);    //差分直接计算结果
        }
    }
    cout<<ans<<endl;    //打印结果
    
}
;