牛客算法竞赛入门笔记1

2021-10-20:昨天开的新坑,看了前几集感觉还可以,后悔为什么没早点跟着学,以前就感觉到了自己的知识体系太散了,这个课好像是11月还是12月结束,她说能达到icpc铜牌水平,我姑且相信好吧,希望跟着学完能有点进步,不求铜牌,cf先能上个1500吧呜呜呜。

# 模拟,枚举与贪心

字符串 (nowcoder.com)

尺取法(说实话这可能是我第一次见到这个做法,或者第一次知道它的学名),正常暴力想法应该是枚举两个端点,但是这里的右端点其实不用后退,想一下就知道了,因此只要两个端点差速右移就是O(n)的复杂度了

 #include <bits/stdc++.h>
 using namespace std;
 int main(){
     map<char,int>x;
     string a;
     cin>>a;
     int l=0,r=0;
     for(int i=0;i<a.size();i++){
         x[a[i]]++;
         if(x.size()==26){
             r=i;
             break;
         }
     }
     int ans=r-l+1;
     for(int i=0;i<a.size();){       
         if(r!=a.size()-1){
             r++;
             x[a[r]]++;
         }   
         while(x[a[i]]>1){
             x[a[i]]--;
             i++;
         }
         if(x.size()==26){
             ans=min(ans,r-i+1);
         }
         if(r==a.size()-1){
             break;
         }       
     }
     cout<<ans<<endl;
     return 0;
 }

丢手绢 (nowcoder.com)

尺取+1,但是我第一发wa了,90分,因为每一次移动要考虑的不仅是逆时针的最大值,还有顺时针的最大值。

 #include <bits/stdc++.h>
 using namespace std;
 #define ll long long
 ll x[100001];
 int main(){
     int n;
     cin>>n;
     ll sum=0;
     for(int i=0;i<n;i++){
         cin>>x[i];
         sum+=x[i];
     }
     ll l=0,r=0;
     ll now=0,ans=0;
     while(1){
         while(now*2<=sum&&r<=n-1){
             ans=max(now,ans);
             now+=x[r];
             r++;
         }
         while(now*2>sum){
             ans=max(ans,sum-now);//第一发没加这一句
             now-=x[l];
             l++;
         }
         if(r==n){
             break;
         }
     }
     cout<<ans<<endl;
     return 0;
 }

Flip Game (nowcoder.com)

黑白棋问题:

1-每个棋子最多只能翻一次

2-我们枚举第一行的翻法(16种),然后递推出第二第三行的翻法(上一行哪个没翻第二行就翻下面那个),如果第四行都翻过来了,就成功,否则失败

题解里有个位运算的做法贼牛逼,把每一行都看成二进制串,假设我按第一个和第三个,那么对应的按操作的二进制码就是1010,现在我一共要对三个地方取反,第一:本位取反,直接让原码和操作码异或,第二:每个按操作的左边取反,操作码左移一位再异或,第三:每个按操作的右边取反,操作码右移一位再异或。

异或就是不进位的二进制加(好记)

看课程里老师调了半天,而我一眼就看出她那里有问题,给我急得()

 #include <iostream>
 #include <cstdio>
 #define INF 0x3f3f3f3f
 using namespace std;
 ​
 int a[5],b[5];
 int ans=INF;
 ​
 int count_1_num(int x){
     int num=0;
     while(x>=1){
         num+=x%2;
         x/=2;
     }
     return num;
 }
 ​
 int fun(int*x){
     int t[5];
     //枚举16种第一行的情况
     for(int i=0;i<16;i++){
         //计算有几个1,就代表翻转了几次
         int num=count_1_num(i);
         for(int i=0;i<4;i++){
             t[i]=x[i];
         }
         t[0]=(t[0]^(i)^(i<<1)^(i>>1));
         t[1]=(t[1]^(i));
         t[0]=(t[0]&15);
         for(int k=1;k<4;k++){
             num+=count_1_num(t[k-1]);
             t[k]=(t[k]^(t[k-1])^(t[k-1]<<1)^(t[k-1]>>1));
             t[k+1]=(t[k+1]^(t[k-1]));
             //把有可能溢出的第五位变成0(这步是我没想到的) 
             t[k]=t[k]&15;       
         }
         if(t[3]==0){
             ans=min(num,ans);
         } 
     }
 }
 ​
 int main(){
     for(int i=0;i<4;i++){
         for(int k=0;k<4;k++){
             char c;
             scanf(" %c",&c);
             if(c=='b'){
                 //把a[i]的第k+1位变成1 
                 a[i]=a[i] | (1<<k); 
             }else{
                 b[i]=b[i] | (1<<k);
             }
         }
     }
     fun(a);
     fun(b); 
     //第一发忘了加impossible居然0分是我没想到的,难道说。。。
     if(ans==INF){
         cout<<"Impossible"<<endl;
         return 0;
     }
     cout<<ans<<endl;
     return 0;
 }


 

 

震惊我一整年的做法,原题数据范围很小,可以把所有学号相加减去来的,但是如果数据范围非常大,学号之和已经超过long long了,就不能这样做了

于是,它来了,位运算:加减相消的原理和两次异或不变的原理是互通的,所以我们可以把所有的学号异或起来,然后再异或掉来的人的学号,剩下的就是没来的。

艹,太帅了,我直接拜发明这个算法的人为师,orz

再升级一下,如果两个人没来呢?

先和上面一样操作一遍得到两个没来的人的学号异或值,找到一位为1的,表明这两个人在这一位上不同,就可以将原数分为两块,每一块都是一个人没来的问题。

ohhhhhh,130没白花,长见识了。

Problem - 333E - Codeforces

这题听他讲好像没什么,一搜2500,妈耶,我到底在学什么,我一个1300分的人也配想2500的题?

思路很巧妙,我们枚举两两点之间的距离,将其从大到小排序,从长边到短边,构成的第一个三角形的最短边长的一半就是答案

但是。我们怎么判断什么时候构成一个三角形呢,最质朴的想法就是我每走过一条边都记录下每个点对应的已经检验过的邻点,如果出现一条边AC,A已经走过B,C也已经走过B,就成了,但是如果正常记录遍历的话必定会超时,怎么办呢? 震惊我两整年的想法来了,c++里有一个bitset类,就是一个很长的二进制串,我们存走过的点时,给它转化成对应的bitset位的值,然后每条新边来的时候判断一下两个端点的bitset的位与,如果不为零,就说明他们之前经过过同一个点,齐活!

我居然自己写出来了,艹,牛逼,去补一下bitset的接口(零碎点那个文档里)

#include <bits/stdc++.h>
 using namespace std;
 pair<double,double>x[3001];
 struct node{
     int x,y;
     double dis;
 };
 node t[10000001];
 bitset<3001>ans[3001];
 ​
 double distance(pair<double,double>x,pair<double,double>y){
     return sqrt((x.first-y.first)*(x.first-y.first)+(x.second-y.second)*(x.second-y.second));
 }
 ​
 bool com(node a,node b){
     return a.dis>b.dis;
 }
 ​
 ​
 int main(){
     int n;
     cin>>n;
     ans[0].reset();
     for(int i=1;i<=n;i++){
         cin>>x[i].first>>x[i].second;
         ans[i].reset();
     }
     int num=0;
     for(int i=1;i<=n;i++){
         for(int j=i+1;j<=n;j++){
             t[num].x=i;
             t[num].y=j;
             t[num].dis=distance(x[i],x[j]);
             num++;
         }
     }
     sort(t,t+num,com);
     for(int i=0;i<num;i++){
         int x=t[i].x;
         int y=t[i].y;
         if((ans[x]&ans[y])!=0){
             printf("%.8lf",t[i].dis/2);
             break;
         }
         ans[x].set(y,1);
         ans[y].set(x,1);
     
     }
     return 0;
 }

月月查华华的手机 (nowcoder.com)

题意:判断序列b是否为a的子序列(不连续)

这题我看到的时候居然想上kmp,艹,绝了,kmp是匹配连续子串,这个是不连续序列,其实就是双指针,但是,这题数据范围很大,所以如过对每个目标串都用一次双指针,就t了。

因此这里用了一个预处理,记录下每个位置,其后面每个字母第一次出现的位置,从后往前扫一遍就有了,感觉有点像ccpc网络赛的那个字符串dp的签到题(一模一样好伐)。

 #include <bits/stdc++.h>
 using namespace std;
 ​
 int x[1000001][30];
 int last[30];
 ​
 int main(){
     string a;
     cin>>a;
     memset(last,-1,sizeof(last));
     for(int i=a.size()-1;i>=0;i--){
         for(int k=0;k<26;k++){
             x[i][k]=last[k];
         }
         last[a[i]-'a']=i;
     }
     int t;
     cin>>t;
     while(t--){
         string b;
         cin>>b;
         int pos=last[b[0]-'a'];
         int beg=pos;
         if(pos<0){
             cout<<"No"<<endl;
             continue;
         }
         int flag=1; 
         for(int i=1;i<b.size();i++){
             pos=x[pos][b[i]-'a'];
             if(pos<0){
 //              pos=x[beg][b[0]-'a'];//这里就是因为没想明白串和序列,想回退
 //              beg=pos;
                 if(pos<0){
                     flag=0;
                     cout<<"No"<<endl;
                     break;
                 }
                 //i=0;
             }
         }
         if(flag==1){
             cout<<"Yes"<<endl;
         }
     }
     return 0;
 } 

Problem - 484A - Codeforces

题意:找l-r内二进制1数量最多的数

位运算+贪心,因为数据范围太大,必不可能遍历,所以考虑贪心,把左右边界都变为二进制,把左边界为0的二进制位一个个改成1,看是否还小于右边界即可。tql,我自己想绝对想不出来,但是实现是我自己写的,我也算是稍微了解一些位运算了吧(

 #include <bits/stdc++.h>
 using namespace std;
 #define ll long long
 int main(){
     int t;
     cin>>t;
     while(t--){
         ll l,r;
         cin>>l>>r;
         ll ans=l;
         for(ll i=1;i<=r;i<<=1){
             if((l&i)==0){
                 ans+=i;
                 if(ans>r){
                     ans-=i;
                     break;
                 }
             }
         }
         cout<<ans<<endl;
     }   
     return 0;
 }

兔子的区间密码 (nowcoder.com)

题意:求l-r内两个数的异或的最大值

找到l,r的第一个不一样的二进制位,从这位开始后面可以保证一个数全是1,另一个全是0.

 #include <bits/stdc++.h>
 using namespace std;
 #define ll long long
 int main(){
     int t;
     scanf("%d",&t);
     while(t--){
         ll a,b;
         scanf("%lld %lld",&a,&b);
         int i=63;
         for(;i>=0;i--){
             if((a>>i)!=(b>>i)){
                 break;
             }
         }
         printf("%lld\n",(1LL<<i+1)-1);
     }
     return 0;
 }

位运算:一位一位去考虑

递归与分治

表达式计算4 (nowcoder.com)

题意就跟标题一样,计算表达式,当时正好数据结构也布置了一个类似的作业,思路是每一次都找到运算级最低的那个符号,将表达式分为两个部分,分别计算。

 #include <bits/stdc++.h>
 using namespace std;
 string a;
 //string转int 
 int num(int l,int r){
     int ans=0;
     for(int i=l;i<=r;i++){
         ans*=10;
         ans+=a[i]-'0';
     }
     return ans;
 }
 //计算l到r的计算式 
 int calc(int l,int r){
     int pos1=-1,pos2=-1,pos3=-1;
     int cnt=0;
     //找到不在括号里的优先级最低的运算符的位置 
     for(int i=l;i<=r;i++){
         if(a[i]=='('){
             cnt++;
         }
         if(a[i]==')'){
             cnt--;
         }
         if(cnt==0){
             if(a[i]=='+'||a[i]=='-'){
                 pos1=i;
             }
             if(a[i]=='*'||a[i]=='/'){
                 pos2=i;
             }
             if(a[i]=='^'){
                 pos3=i;
             }
         }       
     }
     //如果没找到运算符 
     if(pos1==-1&&pos2==-1&&pos3==-1){
         //要么是被括号整体括起来了 
         //注:后面两个else if是为了排除多余的括号 
         if(cnt==0&&a[l]=='('){
             return calc(l+1,r-1);
         }else if(cnt<0&&a[l]=='('){
             return calc(l,r-1);
         }else if(cnt>0&&a[r]==')'){
             return calc(l+1,r);
         }
         //要么是就是一个数字 
         return num(l,r);
     }
     //+ -优先级最低,先判断 
     if(pos1!=-1){
         if(a[pos1]=='+'){
             return calc(l,pos1-1)+calc(pos1+1,r);
         }else{
             return calc(l,pos1-1)-calc(pos1+1,r);
         }
     }   
     //然后是* / 
     if(pos2!=-1){
         if(a[pos2]=='*'){
             return calc(l,pos2-1)*calc(pos2+1,r);
         }else{
             return calc(l,pos2-1)/calc(pos2+1,r);
         }
     }
     //最后是乘方
     if(pos3!=-1){
         return pow(calc(l,pos3-1),calc(pos3+1,r)); 
     }
 }
 ​
 int main(){
     cin>>a;
     cout<<calc(0,a.size()-1)<<endl;
     return 0;
 }

1027-kotori和糖果_2021秋季算法入门班第二章习题:递归、分治 (nowcoder.com)

题意:共n堆糖果,一堆一个,合并两堆数量不一样的花费1,求最少总花费

这题本来是个很简单的记忆化搜索,但是我没想清楚应该返回什么,我本来想着返回两端的长度,但是后来转念一想,不对啊,有了n两端长度不是都有了吗,所以应该返回的是形成这两端需要的操作数。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
map<ll,ll> mp;
ll getans(ll n){
    if(n==0) return 0;
    if(mp.count(n)) return mp[n];
    if(n&1){
        mp[n]=2*getans(n/2);
    }else{
        mp[n]=getans(n/2)+getans(n/2-1)+1;
    }
    return mp[n];
}
int main(){
    ll t;
    scanf("%lld",&t);
    while(t--){
        ll n;
        scanf("%lld",&n);
        if(n==0)puts("0");
        else printf("%lld\n",getans(n-1));
    }
}

二分、三分、01分数规划

艹,我记得我写完这一部分的了,但是好像关电脑前忘了保存,然后就没了,心态爆炸,再写一遍,呜呜呜

小咪买东西 (nowcoder.com)

01分数规划经典题目,题意:给n个物品,每一个物品都有一个价值和花费,求买k个的最大总价值(val)和总花费之比(cos)

思路:我们假设这个最大比值是x,那么比x小的比值一定也存在这样的方案,比x大的比值一定不存在这样的方案,那么我们就可以考虑二分,对于一个比值num来说,如果可行,一定存在一个方案使得val/cos>=num-->val-numcos>=0;因此我们在判断的时候把所有val-numcost最大的k个值算在内,如果这样都不行那就是真的不行。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
struct node{
	ll c,v,mar;
};
ll n,m;
node x[10001];
bool com(node a,node b){
	return a.mar>b.mar;
}
bool fun(ll a){
	ll num=0;
	for(ll i=0;i<n;i++){
		x[i].mar=x[i].v-a*x[i].c;
	}
	sort(x,x+n,com);
	ll sum=0;
	for(ll i=0;i<m;i++){
		sum+=x[i].mar;
	}	
	if(sum<0){
		return false;
	}else{
		return true;
	}
}

int main(){
	ll t;
	cin>>t;
	while(t--){		
		cin>>n>>m;
		for(ll i=0;i<n;i++){
			cin>>x[i].c>>x[i].v;
		}
		ll l=0,r=1e8+1;
		ll mid=0;
		while(l<r){
			mid=(l+r+1)/2;
			if(fun(mid)){
				l=mid;
			}else{
				r=mid-1;
			}
		}
		cout<<l<<endl;
	}
	return 0;
}

gpa (nowcoder.com)

和上一题差不多,就是把最大值公式变了一下,我们一样做

#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define ll double
struct node{
	double a,b,tem;
};
node x[100001];

int n,m;

bool com(node a,node b){
	return a.tem<b.tem;
}

bool fun(ll a){
	for(int i=0;i<n;i++){
		x[i].tem=x[i].a*(x[i].b-a);
	}
	sort(x,x+n,com);
	for(int i=0;i<m;i++){
		if(x[i].tem<0){
			x[i].tem=-INF;
		}
	}
	ll sum=0;
	for(int i=0;i<n;i++){
		if(x[i].tem==-INF){
			continue;
		}
		sum+=x[i].tem;
	}
	if(sum<0){
		return false;
	}else{
		return true;
	}
}

int main(){	
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>x[i].a;
	}
	for(int i=0;i<n;i++){
		cin>>x[i].b;
	}
	double l=0,r=1e11+1;
	while(r-l>1e-6){//这里答案是小数,所以我们用控制精度的方法
		double mid=(l+r)/2;
		if(fun(mid)){
			l=mid;
		}else{
			r=mid;
		}
	}
	printf("%.6lf\n",l);
	return 0;
}

堆栈、队列、单调栈、单调队列

[NOIP2016]蚯蚓 (nowcoder.com)

题意:给你n个数,每次会将一个数分割成比例为定值的两个数,求m次分割怎么让每个数尽可能小,每个数还会随时间增长一定值。

这题本来觉得没啥难的,优先队列嘛,谁不会啊(),但是看了眼数据范围,完蛋,会t,没办法,只能听雨巨讲了。

思路真的很巧妙,假设我们将最初的序列排序,拆分之后将两段分别放进两个队列里面,因为我们每次拆分的一定都比上一次小(或者一样)因此拆分后的两段一定也比先前的两段小,所以我们后放入队列的一定比先放入的小,这样就保证了有序,每次选择最大的时候只要去找三个队列的头即可。tql,未曾设想的道路。

但是这个输出要求就很恶心,我好不容易写完,你非要在输出上折磨我是什么意思

#include <bits/stdc++.h>
#include <vector>
#include <queue>
using namespace std;
#define ll long long

int main() {
	queue<int>q[3];
	int x[100001];
	ll n, m, Q, u, v, t;
	cin >> n >> m >> Q >> u >> v >> t;
	double p = (double)u / v;
	for (int i = 0; i < n; i++) {
		cin >> x[i];
	}
	sort(x, x + n, greater<ll>());
	for (int i = 0; i < n; i++) {
		q[0].push(x[i]);
	}
	int now = 0;
	while (now < m) {
		int mn = -0x3f3f3f3f, maxindex=-1;
		for (int i = 0; i < 3; i++) {
			if (!q[i].empty() && q[i].front() > mn) {
				mn = q[i].front();
				maxindex = i;
			}
		}
		if (maxindex==-1) {
			break;
		}
		mn += now * Q;
		q[maxindex].pop();
		q[1].push((int)(u * mn/v) - now * Q-Q);
		q[2].push(mn - (int)(u * mn/v) - now * Q-Q);
		
		if ((now+1) % t == 0) {
			cout << mn << " ";
		}
		now++;
	}
	cout << endl;
	now=1;
	while (!q[0].empty() || !q[1].empty() || !q[2].empty()) {
		int mn = -0x3f3f3f3f, maxindex=-1;
		for (int i = 0; i < 3; i++) {
			if (!q[i].empty() && q[i].front() > mn) {
				mn = q[i].front();
				maxindex = i;
			}
		}
		if (maxindex==-1) {
			break;
		}
		if(now%t==0){
			cout << mn+m*Q << " ";
		}	
		now++;
		q[maxindex].pop();
	}
	return 0;
}

Sliding Window (nowcoder.com)

动态维护一个在i-k~i之间可能成为答案的队列。

以最大值为例,如果后一个比前一个大,那么前一个就失去了成为答案的机会,就可以出队了,反之就不出队,但是当这个值的位置比区间的左端点还小的时候也要出队,本来我想用队列存数据的,但是雨巨点醒了我,存数据的话还要直到下标,所以我们直接把下标存起来,到原数组里找对应的数据即可。

第一次用对拍,有点爽,第一次交的时候wa在了长度为1的时候。

#include <bits/stdc++.h>
using namespace std;
int x[1000011];
int main(){
//	freopen("a.in","r",stdin);
//	freopen("std.out","w",stdout);
	int n,k;
	cin>>n>>k;
	for(int i=0;i<n;i++){
		cin>>x[i];
	}
	if(n==1){
		cout<<x[0]<<endl<<x[0];
		return 0;
	}
	deque<int>q;
//    q.push_back(0);第一次wa在这.以后别tm提前插入行不行,对拍的时候是很爽,但是写起来很麻烦
	for(int i=0;i<n;i++){
		while(!q.empty()&&x[q.back()]>x[i]){
			q.pop_back();
		}
		q.push_back(i);
		if(q.front()<=i-k){
			q.pop_front();
		}
		if(i+1>=k){
			cout<<x[q.front()]<<" ";
		}	
	}
	cout<<endl;	
	q.clear();
	for(int i=0;i<n;i++){
		while(!q.empty()&&x[q.back()]<x[i]){
			q.pop_back();
		}
		q.push_back(i);
		if(q.front()<=i-k){
			q.pop_front();
		}
		if(i+1>=k){
			cout<<x[q.front()]<<" ";
		}
	}
	return 0;
}

Largest Rectangle in a Histogram (nowcoder.com)

单调栈的板子题,所谓单调栈就是我们维护一个单调的栈,每当有新数据进来时,如果它破坏了原有的单调性,我们就一直出栈直到单调为止。

题意:给你一串高度不等高的等宽柱子,求能够在里面划出的最大的矩形的面积。

思路:我们存下每个柱子往左能够延伸的最大长度,如果新加的柱子比之前的高,那它的最大宽度就是1,如果新加的柱子比之前的矮,那我们在出栈的过程中把每一个出栈的柱子的往左延伸的最大长度相加得到的就是这个新柱子的最大长度,当然,如果一个柱子出栈了,就说明它不可能再向右延申了,这时我们用高度×它能向左延申的最大长度就是一个可能的最大面积。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
int x[100001];
int num[100001];//记录每个柱子能够向左延伸的最大长度
int main(){
	int n;
	while(cin>>n&&n){
		for(int i=1;i<=n;i++){
			cin>>x[i];
		}
		stack<ll>stk;
		ll ans=0;
		x[++n]=0;
		for(int i=1;i<=n+1;i++){
			ll temp=0;
			while(!stk.empty()&&x[i]<=x[stk.top()]){
				temp+=num[stk.top()];
				ans=max(ans,temp*x[stk.top()]);
				stk.pop();
			}
			stk.push(i);
			num[stk.top()]=temp+1;
		}
		cout<<ans<<endl;
	}
	return 0;
} 

Guess you like

Origin blog.csdn.net/m0_58178876/article/details/121387523