第二次训练题解
第二次相比第一次难度有一点的提升加入了一些数据结构的内容
A 兔子的逆序对
知识点 :归并排序、数学知识
首先我们采用归并排序求出逆序对的数量,这个看了大家写的代码大家都会求就不多讲了这里放出我写的归并求逆序对的代码可以供大家参考下
大家特别要注意对于逆序对的数量记得开long long
ll merge_sort(int l,int r) {
if(l==r) return 0;
int mid=l+r>>1;
ll ans=merge_sort(l,mid)+merge_sort(mid+1,r);
int k=0,i=l,j=mid+1;
while(i<=mid&&j<=r) {
if(q[i]<=q[j]) tmp[k++]=q[i++];
else {
tmp[k++]=q[j++];
ans+=mid-i+1;
}
}
while(i<=mid) tmp[k++]=q[i++];
while(j<=r) tmp[k++]=q[j++];
for(int i=l,j=0;i<=r;i++,j++){
q[i]=tmp[j];
}
return ans;
}
接下来就是这个题的关键之处
对于一个序列我们有个常识可知:正序对的数量加上逆序对的数量会等于任意两个数(下标i>j)组成序对的数量
并且区间内的数字反转并不会影响序列总的逆序对数量因为相对位置并没有改变。
就比如 1、3 、2
任意两两组合可以得到的数量为n*(n-1)/2
也就是3*2/2=3
正序对为(1,3),(1,2)
逆序对为(3,2)
于是我们就先设一个序列中总的逆序对数量为ans
需要修改区间中的逆序对数量为x
修改区间中总的序对数目为(r-l+1)(r-l)/2
修改区间中的正序对数量为cnt
于是有等式:x+cnt=(r-l+1)*(r-l)/2
并且修改区间外的逆序对数量:ans-x
当修改过后区间中的正序对变成逆序对
所以整个序列中的逆序对数量为ans-x+cnt
于是化简可以得到最后序列逆序数为
ans-x+(r-l+1) * (r-l)/2-x=ans-2*x+(r-l+1) * (r-l)/2
通过这个式子可知2x不影响奇偶可以直接去掉那么影响奇偶的就只有ans和(r-l+1)*(r-l)/2了也就是总共区间中的逆序对数量和区间内的总序对数量。
std
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
int n,m,k,l,r,x,t,y;
int a[maxn],sum[maxn],q[maxn],tmp[maxn];
ll merge_sort(int l,int r) {
if(l==r) return 0;
int mid=l+r>>1;
ll ans=merge_sort(l,mid)+merge_sort(mid+1,r);
int k=0,i=l,j=mid+1;
while(i<=mid&&j<=r) {
if(q[i]<=q[j]) tmp[k++]=q[i++];
else {
tmp[k++]=q[j++];
ans+=mid-i+1;
}
}
while(i<=mid) tmp[k++]=q[i++];
while(j<=r) tmp[k++]=q[j++];
for(int i=l,j=0;i<=r;i++,j++){
q[i]=tmp[j];
}
return ans;
}
int main() {
//1 4 2 3
int flag=1;
n=read();
for(int i=0;i<n;i++){
q[i]=read();
}
t=read();
int ans=merge_sort(0,n-1);
if(ans%2==0){
flag=0;
}
while(t--){
l=read();
r=read();
int pp=(r-l+1)*(r-l)>>1;
if(pp%2!=0) flag^=1;
if(flag==0){
printf("like\n");
}
else{
printf("dislike\n");
}
}
}
B、C都是模拟水题就不讲了
D 完全平方数
题意就是要我们找出区间中可以开平方的数有多少个
于是那我们就对区间两个端点就行开根号处理
因为左右端点开完根号后形成的区间中的数平方一定能在原区间中找到
但是要注意的是因为sqrt是会自动四舍五入的所以如果左端点开的实际根号数会大于整数,就比如5开根号是2.几左右会自动四舍五入为2这样区间就会多算一个1个数所以我们需要判定一下然后减一。对于右端点是不影响的
std
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
const int INF=0x3f3f3f;
//int n,m,k,l,r,x,t,y;
inline int read() {
int X=0;
bool flag=1;
char ch=getchar();
while(ch<'0'||ch>'9') {
if(ch=='-') flag=0;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
X=(X<<1)+(X<<3)+ch-'0';
ch=getchar();
}
if(flag) return X;
return ~(X-1);
}//快读
inline void write(int X) {
if(X<0) {
X=~(X-1);
putchar('-');
}
if(X>9) write(X/10);
putchar(X%10+'0');
}//快输
//函数预定义区
int main() {
//IOS;
ll n,l,r;
ll p,q;
cin>>n;
while(n--) {
cin>>p>>q;
ll i=sqrt(p);
ll ans=0;
if(i*i!=p){
ans-=1;
}
ll j=sqrt(q);
ans+=j-i+1;
cout<<ans<<endl;
}
}
E 栈和排序
知识点:模拟、贪心
这题大家的通过率都还可以那我就简单讲下
这题就是想让输出来的数的字典序尽可能大,如果能完全从大到小输出来那当然最好
于是我们从数组后面开始统计后缀最大值,接着使用栈来判定我们的栈顶是否比后缀最大值更大如果更大则该出栈了,否则会让结果字典序变小
std
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+2;
int n,a[N], maxn[N],sta[N];
int main()
{
int top=0;
scanf("%d",&n);
int i;
for(i=0;i<n;i++)
scanf("%d",&a[i]);
for(i=n-1;i;i--)
maxn[i]=max(maxn[i+1],a[i]);
for(int i=0;i<n;++i)
{
sta[++top]=a[i];
while(top&&sta[top]>maxn[i+1])
{
printf("%d ",sta[top--]);
}
}
}
F wyh的物品
知识点:二分
这个题是经典的二分答案类型的题,我们通过猜答案来一步步逼近正确的答案并使正确的答案更符合题意
对于这个题我们直接二分单位价值然后让每个物品的总价值减去总重量*单位价值,最后排序选取前k个相加判断是否大于等于0
对于这个大于0就等于想知道这个单位价值我们是否能得到如果全都前k个加起来小于0说明这个单位价值我们取的太大了需要减小
若大于等于0则符合条件我们使左边的端点变为中点使答案更大
若小于0则不符合使右边的端点选取更小的单位价值
时间复杂度(nlog(n))
std
#include <bits/stdc++.h>
using namespace std;
const int maxn=10e5;
const int INF=1e9;
typedef long long ll;
int c[maxn],v[maxn];
double sum[maxn];
int t,n,m,k;
bool cmp(double a,double b)
{
return a>b;
}
bool check(double avg)
{
for(int i=0;i<n;i++)
{
sum[i]=v[i]-c[i]*avg;
}
sort(sum,sum+n,cmp);
double sumv=0;
for(int i=0;i<k;i++)
{
sumv+=sum[i];
}
return sumv>0;
}
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d %d",&n,&k);
for(int i=0;i<n;i++)
{
scanf("%d %d",&c[i],&v[i]);
}
double l=0,r=INF;
for(int i=0;i<100;i++)
{
double mid=(l+r)/2;
if(check(mid))
{
l=mid;
}
else
{
r=mid;
}
}
printf("%.2lf\n",r);
}
}
G 图的遍历
知识点:图的建立、连通块、染色法、判环、dfs深搜,
这题应该算是这里面最难的了牵扯到的知识点也非常的多大家现阶段做不出来也很正常o( ̄▽ ̄)ブ。
关于图的定义我稍微提一下本质上就是一些点和边的集合,点通过边连接了起来。
边呢有无向边和有向边像这个题就是无向边,顾名思义就是两个点可以互通。
有向边就是有方向的只能一个点到另一个点,他们两总有一方不能通过这个边到达另一个点。
对于图的存储方式像这题不带边权所以我建议使用vector来存每个点与点之间的关系相当于邻接表来存储了因为这样比较好dfs。
解决完图的存储问题,要做这个题之前还得知道连通块的概念,如果有一部分点相互联通那么他们就是联通块,用题目中的样例来解释
5 4
1 2
2 3
3 4
4 5
5个点4条边
像这种的就不是连通块因为5跟1并不联通
但如果你将1跟4,5任意相连他就是个连通块了
接着来说下奇数环和偶数环
奇数环就是有奇数个点的环
偶数环同理
这题要我们每次只能走两步
所以这题就是要判断是否有奇数环,因为有奇数环我们可以通过这个环走到任何一个地方
举个栗子:
1可以通过这个奇数环将所有点遍历掉
于是这题就转变为求连通块数量和是否有奇数环的问题了😊
如果没有奇数环我们需要人工加一条,已经含有则不需要,对于n个联通块需要n-1条边
std
#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define R __int128
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const ll INF=0x7f7f7f7f;
const int maxn=1e6+10;
const ll mod=1e9+7;
const int N=9;
ll read() {
ll f=0,x=0;
char ch=getchar();
while(!isdigit(ch)) {
if(ch=='-') f=1;
ch=getchar();
}
while(isdigit(ch)) {
x=x*10+ch-'0';
ch=getchar();
}
return !f?x:-x;
}
ll qpow(ll a,ll b,ll mod) {
ll ans=1;
while(b) {
if(b&1) ans=ans*a%mod;
a=a*a%mod;
b>>=1;
}
return ans;
}
ll inv(ll x,ll mod) {
return qpow(x,mod-2,mod);
}
int vis[maxn],s[maxn],flag=1;
ll n,t,k,m,d;
vector<int> g[maxn];
void dfs(int x){
//染色法判断是否有奇环
for(int i=0;i<g[x].size();i++){
int j=g[x][i];
if(vis[j]==0){
//如果没有遍历过则染成与邻接点不同颜色
s[j]=!s[x];
vis[j]=1; //遍历过了
dfs(j);
}
else if(s[j]==s[x]) flag=0; //代表出现了奇环
}
}
void solve(){
int ans=0;
cin>>n>>m;
while(m--){
int a,b;
cin>>a>>b;
g[a].push_back(b);
g[b].push_back(a);
}
for(int i=1;i<=n;i++){
if(vis[i]==0){ //判连通块
vis[i]=1;
ans++;
dfs(i);
}
}
cout<<ans+flag-1<<endl;
}
int main() {
solve();
}
/*
* ┌───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┐
* │Esc│ │ F1│ F2│ F3│ F4│ │ F5│ F6│ F7│ F8│ │ F9│F10│F11│F12│ │P/S│S L│P/B│ ┌┐ ┌┐ ┌┐
* └───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┘ └┘ └┘ └┘
* ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐
* │~ `│! 1│@ 2│# 3│$ 4│% 5│^ 6│& 7│* 8│( 9│) 0│_ -│+ =│ BacSp │ │Ins│Hom│PUp│ │N L│ / │ * │ - │
* ├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─────┤ ├───┼───┼───┤ ├───┼───┼───┼───┤
* │ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{ [│} ]│ | \ │ │Del│End│PDn│ │ 7 │ 8 │ 9 │ │
* ├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤ └───┴───┴───┘ ├───┼───┼───┤ + │
* │ Caps │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter │ │ 4 │ 5 │ 6 │ │
* ├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────────┤ ┌───┐ ├───┼───┼───┼───┤
* │ Shift │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│ Shift │ │ ↑ │ │ 1 │ 2 │ 3 │ │
* ├─────┬──┴─┬─┴──┬┴───┴───┴───┴───┴───┴──┬┴───┼───┴┬────┬────┤ ┌───┼───┼───┐ ├───┴───┼───┤ E││
* │ Ctrl│ │Alt │ Space │ Alt│ │ │Ctrl│ │ ← │ ↓ │ → │ │ 0 │ . │←─┘│
* └─────┴────┴────┴───────────────────────┴────┴────┴────┴────┘ └───┴───┴───┘ └───────┴───┴───┘
*/
H kmp模板题
这个大家看csdn的博客讲解可能比我讲的更为清晰这个放在这里只是让大家能有个印象代码可以参考我的
时间复杂度O(n+m)
std
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int ne[maxn],n,m;
char s[maxn],p[maxn];
int main() {
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
// cin>>s+1;
scanf("%s",s+1);
// getchar();
// cin>>p+1;
scanf("%s",p+1);
int n=strlen(s+1);
int m=strlen(p+1);
for(int i=2,j=0;i<=m;i++){
while(j&&p[i]!=p[j+1]) j=ne[j];
if(p[j+1]==p[i]) j++;
ne[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==m){
printf("%d\n",i-j+1);
// cout<<i-j+1<<endl;
}
}
for(int i=1;i<=m;i++){
// cout<<ne[i]<<" ";
printf("%d ",ne[i]);
}
}
I Big Water Problem
其实这题我没想到牛客的数据这么水能让O(nm)的复杂度过
让有些同学前缀和过掉了,实际上在这个数据范围内O(nm)使必不可能过的,但是这也是敢于尝试的奖励吧哈哈
这里题目想要考察的知识点使线段树的区间查询和单点修改
这里直接给出我的代码代码中都有注释
std
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m;
int w[N];//记录一下权重
struct node{
int l,r;//左右区间
int sum;//总和
}tr[N*4];//记得开 4 倍空间
void push_up(int u)//利用它的两个儿子来算一下它的当前节点信息
{
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//左儿子 u<<1 ,右儿子 u<<1|1
}
void build(int u,int l,int r)/*第一个参数,当前节点编号,第二个参数,左边界,第三个参数,右边界*/
{
if(l==r)tr[u]={l,r,w[r]};//如果当前已经是叶节点了,那我们就直接赋值就可以了
else//否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点
{
tr[u]={l,r};//这里记得赋值一下左右边界的初值
int mid=l+r>>1;//边界的话直接去计算一下 l + r 的下取整
build(u<<1,l,mid);//先递归一下左儿子
build(u<<1|1,mid+1,r);//然后递归一下右儿子
push_up(u);//做完两个儿子之后的话呢 push_up 一遍u 啊,更新一下当前节点信息
}
}
int query(int u,int l,int r)//查询的过程是从根结点开始往下找对应的一个区间
{
if(l<=tr[u].l&&tr[u].r<=r)return tr[u].sum;//如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了
//否则的话我们需要去递归来算
int mid=tr[u].l+tr[u].r>>1;//计算一下我们 当前 区间的中点是多少
//先判断一下和左边有没有交集
int sum=0;//用 sum 来表示一下我们的总和
if(mid>=l)sum+=query(u<<1,l,r);//看一下我们当前区间的中点和左边有没有交集
if(r>=mid+1)//看一下我们当前区间的中点和右边有没有交集
sum+=query(u<<1|1,l,r);
return sum;
}
void modify(int u,int x,int v)//第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值
{
if(tr[u].l==tr[u].r)tr[u].sum+=v; //如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了
//否则
else
{
int mid=tr[u].l+tr[u].r>>1;
//看一下 x 是在左半边还是在右半边
if(x<=mid)modify(u<<1,x,v);//如果是在左半边,那就找左儿子
else modify(u<<1|1,x,v);//如果在右半边,那就找右儿子
//更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 pushup 一遍
push_up(u);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&w[i]);
build(1,1,n);/*第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n */
//后面的话就是一些修改操作了
while(m--)
{
int k,a,b;
scanf("%d%d%d",&k,&a,&b);
if(k==2)printf("%d\n",query(1,a,b));//求和的时候,也是传三个参数,第一个的话是根节点的编号 ,第二个的话是我们查询的区间
//第一个参数也就是当前节点的编号
else
modify(1,a,b);//第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值
}
return 0;
}