牛客网寒假算法基础训练营2学习笔记

这篇博客记录了在牛客网寒假算法基础训练营的学习过程,涉及计算几何中求三角形面积的叉积方法和C++中找到顺序第k个值的算法。此外,还介绍了无序关联容器unordered_map的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

处女座的签到题
在这里插入图片描述
一道计算几何题;
考点在于知道三角形的坐标如何求其面积以及如何在一串无序数据中找到顺序第k的值;
○1那么这里使用的计算三角形面积的方法是叉积,也就是向量乘法(将同一点引出的两边作为向量)面积即为两向量相乘的绝对值的二分之一。
○2如何找到顺序第k的值是有o(n)的算法的,其核心思想类似于快速排序(二分递归,每次找一个数,把比他小的数放左边,比他大的放右边,再两边递归来继续排序)只不过找顺序的时候只需要向一边靠拢。C++直接就有现成的函数可供使用——nth_element(首地址,首地址加顺序-1,末地址);

具体做法就是先遍历所有情况,将所有面积算出来再找顺序第k的面积;
代码如下:

#include <bits/stdc++.h>
using namespace std;
const int mx=110;
typedef pair<int,int> PII;
PII points[mx];
vector <long double>areas;
long double cal_area(PII a, PII b , PII c) //叉积计算面积
{
    long double x1 = b.first - a.first , y1=b.second - a.second,
    x2 = c.first - a.first , y2 = c.second - a.second;
    //cout << fabs( x1*y2 - x2*y1 )/2 << endl;
    return fabs( x1*y2 - x2*y1 )/2;
}
        int n,k;
int main()
{
    int T;
    cin >> T; //读入样例数
    while(T--)
    {

        cin >> n >> k; 
        areas.clear();
        for(int i=0;i<n;i++)cin >> points[i].first >> points[i].second;
        for(int i=0;i<n;i++)
            for(int j=0;j<i;j++)
                for(int h=0;h<j;h++) //遍历所有三点可能取到的情况,注意i、j、h的取值以防止重复枚举
                    areas.push_back(-cal_area(points[i],points[j],points[h]));
        //计算,输入负数是因为nth_element默认第k小,而我们要第k大
        nth_element(areas.begin(),areas.begin()+k-1,areas.end());//nth_element用法实例
        printf("%.2llf\n",-areas[k-1]);//输出记得再取一次负数
    }
    return 0;
}

处女座与cf

在这里插入图片描述
一道模拟题,没什么好说的,按题意模拟即可;
学到的知识点是哈希表(aka 无序关联容器)——unordered_map(头文件<unordered_map>)的运用。
代码如下:

#include <bits/stdc++.h>
using namespace std;
unordered_map <string,unordered_map<string,vector<pair<int ,string>>>>users;//哈希表申明实例
//哈希表可以套哈希表,数据结构可以套数据结构
vector<pair<int,string>>ranks; //排名向量数组
int get_score(string problem) //获取问题的总分
{
    return (problem[0]-'A')*500 +500;
}

int main()
{
    int n,m;
    cin >> n >> m;
    for(int i=0;i<n;i++)//输入用户名字顺便初始化(初始化可以省略)
    {
        string name;
        cin >> name;
        users[name]=unordered_map<string,vector<pair<int ,string>>>();
    }
    for(int i=0;i<m;i++)//将每个提交记录存储到对应的用户名下的problem哈希表中
    {
        int time;
        string user,problem,verdict;
        cin >> time >> user >> problem >> verdict;
        users[user][problem].push_back({time,verdict});//将读到的数据压到哈希表中
    }
    
    for(auto user : users)//遍历users哈希表
    {
        if(user.second.empty())continue;//如果该用户没有数据,说明他没有提交,则跳过
        int score=0;
        for(auto problem : user.second)//针对每个用户遍历他的problem
        {
            auto records=problem.second;//取problem中的数据向量(包含提交时间和提交状态)
            int cnz=0;
            for(int i=0;i+1<records.size();i++)//计数一个问题的总提交次数
            {
                if(records[i].second!="CE")cnz++;
            }
            auto back = records.back();//取最后一次提交
            if(back.second == "AC")//AC才有分数
            {
                int ps=get_score(problem.first);
                score+=max((int)(ps*0.3),ps-back.first*ps/250-cnz*50);
            }
        }
        ranks.push_back({score,user.first});//有提交的才将总分和名字压入ranks向量
    }
    sort(ranks.begin(),ranks.end());
    reverse(ranks.begin(),ranks.end());//倒序取从大到小
    bool has_print = false;//判断是否输出
    int last = 0;
    for(int i=0;i<ranks.size();i++)
    {
        if(i && ranks[i].first != ranks[i-1].first)last =i;//取排名(防止并列导致的计算错误)
        if(ranks[i].second=="cnz")//找到大佬了就输出
        {
            printf("%d\n",ranks[i].first);
            printf("%d/%d",last+1,ranks.size());
            has_print = true;
            break;
        }
    }
    if(!has_print)cout << -1;//找不到输出-1
    return 0;
}

处女座的砝码
在这里插入图片描述
数学题
要求取最小数量的砝码,那么反过来说就是取的砝码利用率可以达到最大。
那么如何让砝码利用率最大,就是砝码们的每一个组合都有效,可以生成一个新的数字(先不要求连续);则对于每一个砝码,其有三种情况(放左边,不放,放右边),因此对于一个m元组砝码的最多组合是3m种,去掉00和左右对称(123,456和456,123)的情况后总共为(3m-1)/2种。M取1,2,3…可得结果为1,4,13,取得的砝码刚好应为1,3,9故很容易想到三进制,即每次取得的砝码为3m即为结果。那么我们只要确定令(3^m-1)/2>n的m的值即可;
证明可以用数学归纳法(你也可以用dp来推),我们带具体数字的话就是m=3时可以包含1-13,m=4时会多出来一个27,14-26的部分可以由27-(1-13)来取得,28-40的部分可以由27+1~13来取得,推广到一般就证明了。
代码如下(要用高精度):

#include <bits/stdc++.h>
using namespace std;


int main()
{
    string x;//读入初始数据
    vector <int>number;
    cin >> x;
    for(int i=x.size()-1;~i;i--)number.push_back(x[i]-'0');//倒放数据,方便进位
    int res=1;
    vector <int> now(1,1);//用来枚举的初始数组
    while(now.size()<number.size() || 
          now.size() == number.size() && vector<int>(now.rbegin(),now.rend()) < vector<int>(number.rbegin(),number.rend()))
    //保证now<number时该式子才进行
    //知识点:vector之间可以直接比较(按字典序)
    //vector倒置的实例
    {//高精度处理数据,让数据每次乘3加1
        int t=1;
        for(int i=0;i<now.size();i++)
        {
            t+=now[i]*3;
            now[i]=t%10;
            t/=10;
        }
        if(t)now.push_back(t);
        res++;//每操作一次都意味着砝码数加1;
    }
    cout << res << endl;//输出砝码数
    return 0;
}

处女座与重修费
在这里插入图片描述
签到题
直接计数小于60分的人数*400即可
代码:

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n,x,ans=0;
    cin >> n;
    for(int i=0;i<n;i++)
    {
        cin >> x;
        if(x<60)ans++;
    }
    cout << ans*400 << endl;
    return 0;
}

处女座与汉明距离
在这里插入图片描述
构造题;

第一点:如何理解汉明距离?
汉明距离即为两个数二进制形式不同的位数;

第二点:如何构造一个满足要求的数列;
我们的思路是由简单到复杂,由一般到特殊;

当m=1时,该数列的构造恰好符合格雷码的构造方式:
我们可以轻松的排列好0~21的情况:0,1;
那么扩展至0~22时将前面的部分倒序排列再每一个数数字二进制的最高位加一个一;
在这里插入图片描述
由此我们可以得到m=1时的数列;

你可以发现,当你把偶数个的数字按位取反后整个序列的汉明距离就会变成m-1;

由上述这个发现,我们可以推导出1<m<n-1时的序列:
{首先我们取h=m+1;然后按照格雷码方法生成1-2h的序列再偶数位取反就可以得到一个1-2m+1的符合要求(汉明距离为m)的序列,然后把它扩展到2n即可;

如何扩展:
类似于格雷码的扩展方法,我们还是在原有的序列的基础上采取倒序再最高位置一的方法来快速生成符合要求的序列,只不过我们不能直接把整个原序列倒序(因为原来的情况是增加一个汉明距离,而现在要求增加m个汉明距离)而是在原序列中寻找一个数使得该数与原序列最末尾的数汉明距离为m-1且该数的前一个数与原序列的头一个数汉明距离也为一,然后我们先把该数(包含该数)一下的部分倒序插到后方然后在倒序放入该数前面的部分的原序列数,同样的这些数全部最高位加一个一;
重复上述操作直到扩展到n为止;
}

代码如下:

#include <iostream>
using namespace std;
const int N = (1 << 20 )+ 10;
int a[N] = {0,1};
int count(int x)//计算1的数量
{
	int cnt = 0;
	while (x)
	{
		if (x & 1)cnt++;
		x >>= 1;
	}
	return cnt;
}
int main()
{
	int n, m;
	cin >> n >> m;
	if (m % 2 == 0)cout << "NO" << endl;
	else
	{
		cout << "YES" << endl;
		for (int i = 1; i < n; i++)
			for (int j = 1 << i; j < (1 << (i + 1)); j++)a[j] = a[(1<<i+1)-1-j] + (1 << i);//格雷码
		if (m == 1)for (int i = 0; i < 1 << n; i++)printf("%d\n", a[i]);
		else
		{
			for (int i = 0; i < 1 << m + 1; i++)
				if (i & 1)a[i] ^= (1 << m + 1)-1;//生成汉明距离为m的前m+1部分
			for (int i = m + 1; i < n; i++)
			{
				int p = -1;
				for (int j = 0; j < 1 << i; j++)
					if (count(a[j] ^ a[(1 << i) - 1]) == m - 1 && count(a[j ?( j - 1) : (1 << i - 1)]) == m - 1)
                    //每次找符合条件的中间数
					{
						p = j;
						break;
					}
				for (int j = 0; j < 1 << i; j++)a[j + (1 << i)] = a[(j + p) % (1 << i)] + (1 << i);
                //找到后插入a数组
			}
			for (int i = 0; i < 1 << n; i++)printf("%d\n", a[i]);
		}
	}
	//system("pause");
	return 0;
}

处女座与宝藏
在这里插入图片描述
2-SAT问题(百度);
方法就是利用图论来解决逻辑问题;
按照题意有n个宝箱m个开关;
那么一开始每一个宝箱对应的状态可以用0,1表示开启/关闭;
对于每一个开关有两种状态摁与不摁;
那么m个开关就对应了am+m种情况;我们可以用a1表示钥匙1使用的情况,用a1+m表示不使用的情况;

逻辑是这样的:
对于每一个宝箱,我们可以先判断它与多少枚钥匙有关;
(1)如果该宝箱与任何一枚钥匙均无关,若该宝箱为关的话显然是无解的;
如此对应的情况就是出现a和a’在一个强连通分量内,那么此情况下只需要随便找一对相反情况连起来表示无解;
(2)如果该宝箱仅与一枚钥匙有关。
如果这个宝箱是关的,那么我们要用钥匙,这意味着我们必须要选择使用钥匙,所以此时当我们查询第i个钥匙时,它只能有一个状态(ai+m)那么我们就建边ai->ai+m这样即使我们遍历时先遍历到ai,建的这条边也会是我们到达ai+m的状态,这就保证了ai/ai+m同时只有一种状态,也是满足条件的状态;
同理,如果该宝箱是开的,则建边ai+m->ai;
(3)与两枚钥匙有关
如果宝箱为开,那么两个钥匙必须同时不用或者同时用:建边ai->bi,ai+m->bi+m
如果宝箱不开,那么两个钥匙同时只能使用一个:建边ai->bi+m,ai+m->bi

最后,检查所有的状态,看看有没有出现正方两种情况同时都能取到的(即ai->ai,ai+m->ai+m同时存在)没有则存在方案,否则就没有方案;

代码如下:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 400010, M = 800010;

int n, m;
vector<int> keys[N / 2];
bool boxes[N / 2];

// N 表示点数,M 表示边数
int h[N], e[M], ne[M], cnt; // 存储有向图, h[]需要初始化成-1
int belong[N], stap[N], stop, instack[N], dfn[N], low[N], bent, dindex;
// bent存储强联通分量的个数,belong[i] 存储第i个点处于哪个强联通分量中

void add(int a, int b)
{
	e[cnt] = b; ne[cnt] = h[a]; h[a] = cnt++;
}

void tarjan(int i)
{
	dfn[i] = low[i] = ++dindex;
	instack[stap[++stop] = i] = 1;
	for (int p = h[i]; p != -1; p = ne[p])
	{
		int j = e[p];
		if (!dfn[j])
		{
			tarjan(j);
			if (low[j] < low[i]) low[i] = low[j];
		}
		else if (instack[j] && dfn[j] < low[i]) low[i] = dfn[j];
	}

	if (dfn[i] == low[i])
	{
		++bent;
		int j;
		do
		{
			j = stap[stop--];
			instack[j] = 0;
			belong[j] = bent;

		} while (j != i);
	}
}
int main()
{

	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%d", &boxes[i]);
	for (int i = 1; i <= m; i++)
	{
		int x;
		scanf("%d", &x);
		while (x--)
		{
			int t;
			scanf("%d", &t);
			keys[t].push_back(i);
		}

	}
	memset(h, -1, sizeof h);

	for (int i = 1; i <= n; i++)
	{
		auto &ks = keys[i];
		if (ks.size() == 0)
		{
			if (boxes[i]) add(1, 1 + m), add(1 + m, 1);
		}
		else if (ks.size() == 1)
		{
			int x = ks[0];
			if (boxes[i]) add(x + m, x);
			else add(x, x + m);
		}
		else if (ks.size() == 2)
		{
			int x = ks[0], y = ks[1];
			if (boxes[i]) add(x, y + m), add(y + m, x), add(y, x + m), add(x + m, y);
			else add(x, y), add(y, x), add(x + m, y + m), add(y + m, x + m);
		}
	}

	for (int i = 1; i <= m + m; i++)
		if (!dfn[i])
			tarjan(i);

	bool success = true;
	for (int i = 1; i <= m; i++)
		if (belong[i] == belong[i + m])
		{
			success = false;
			break;
		}

	if (success) puts("YES");
	else puts("NO");

	return 0;
}

处女座与复读机
在这里插入图片描述
dp题(编辑距离 )
思路是dp[s1][s2]=前s1个字母的A转化为前s2个字符的B的操作数;
状态转移的三种情况:
{
增加:那么可以视为从dp[s1-1][s2]的情况加了一个1;
减少:可视为从dp[s1][s2-1]的情况加1;
替换:则为从dp[s1-1][s2-1]的情况加1;
因为要取最小操作数,那么每次取三者中最小值即可;
}
那么最后的操作数就是dp[A.size()][B.size()];
最后判断操作数,如果操作数小于等于2(小于2的部分可以视为用相同的字母去替换)则输出YES,否则NO(注意大小写,被坑过);

代码如下:

#include<iostream>
#include<cstring>
using namespace std;
string A,B;
char s1[2005],s2[2005];//s1=A , s2=B
int edit[2005][2005];
int dp(int i,int j){ 
    if(edit[i][j]!=-1) return edit[i][j]; 
    if(i==0) return edit[i][j]=j;
    if(j==0) return edit[i][j]=i;
    int bonus=1;
    if(A[i]==B[j]) bonus=0;  //
    return edit[i][j]=min(min(dp(i-1,j)+1,dp(i,j-1)+1),dp(i-1,j-1)+bonus);//取最小值
}
int main(){
    cin>>A>>B;
    memset(edit,-1,sizeof(edit));//初始化数组(-1是为了对付dp时(i-1)越界的情况)
    int len1=A.length(),len2=B.length();
    dp(len1,len2);
    if(edit[len1][len2]<=2)cout << "YES";
    else cout << "NO";
    return 0;
}

处女座的测验(一)
在这里插入图片描述
数学;线性素数筛;
本题要求输出2000个整数,符合以下条件:

  1.   任意两个数互质
    
  2. 任意两个数x,y,满足,其中为n的因子的个数
    

由互质很容易想到质数,如果要求使得任意两个数互质,那么这一串数列的生成肯定跟质数有关。
而为了满足条件2,结合条件1我们就应当想到素数的乘积;

这里提供一个知识点: 一个数可以写成该种形式:N=p1a1 * p2a2 * …*pnan;
那么N的因子数为(a1+1)(a2+1)…(an+1)(每一个因子都有an+1种(0-an)选法);

由此我们可以想到如果这个数列中每一个数都由两个素数乘积组成,那么任意两个数相乘后的因子数为24=16>10则满足题目条件;
则我们只需找到4000个素数两两配对即可,同时题目要求输出的数字不大于4*1e8,故配对时注意小的配大的即可;

另外一个知识点:1~N内的素数个数大约是N/lnN个,因此只要枚举1e5内的数就可以得到超过4000个素数了;

代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
const int mx = 10000010;
int nu[mx] = { 0 }, is_prime[mx];
int get_prime(int n)//线性素数筛
{
	int cnt = 0;
	for (int i = 2; i < n; i++)//从2开始!!!
	{
		if (nu[i] == 0)is_prime[cnt++] = i;//素数标记一下,加入素数数组
		for (int j = 0; j < cnt&&is_prime[j] * i < n; j++)//将和数标记
		{
			nu[i*is_prime[j]]++;
		}
	}
	return cnt;
}
int main()
{
	get_prime(100000);
	//int cnt = 0;
	for (int i = 0, j = 3999; i < j; i++, j--)printf("%d\n", is_prime[i] * is_prime[j])/*,cnt++*/;
	//cout << cnt << endl;
    //cout << get_prime(100000) << endl;
	//system("pause");
	return 0;
}

处女座的测验(二)
在这里插入图片描述
在这里插入图片描述
数论题;
利用上一题的知识点,一个数的因数个数就是可求得;那么直接对每个数分解质因数再枚举法两两组合配对计数总因数小于等于10的数对个数即可;

代码如下:

#include <iostream>
#include <vector>

using namespace std;
typedef pair<int, int>PII;
const int N = 2020;
vector <PII>number[N];
int main()
{
	int n;
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		int in;
		cin >> in;
		for (int j = 2; j*j <= in; j++)//分解质因数
		{
			if (in%j == 0)
			{
				int s = 0;
				while (in%j == 0)s++, in /= j;//计算质因数的幂
				number[i].push_back({ j,s });
			}
		}
		if (in > 1)number[i].push_back({ in,1 });//可能还有一个比较大的质因数;
	}
	int ans = 0;
	for(int i = 0; i < n; i++)
		for (int j = 0; j < i; j++)//两两配对
		{
			auto &A = number[i], &B = number[j];
			int x = 0, y = 0, s = 1;
			while (x < A.size() && y < B.size())
			{
				if (A[x].first < B[y].first)s *= A[x++].second + 1;
				else if (A[x].first > B[y].first) s *= B[y++].second + 1;
				else s *= A[x++].second + B[y++].second + 1;
			}
			while(x < A.size())s *= A[x++].second + 1;
			while (y < B.size())s *= B[y++].second + 1;
			if (s <= 10)ans++;//小于等于10的计数一下
		}
	cout << ans << endl;
	system("pause");
	return 0;
}

处女座的期末复习
在这里插入图片描述
贪心;
每次先选最近考试的科目,假设一复习完就立刻考试,然后开始下一科复习,那么,只要总的复习时间小于考试时间,就可以复习的完。
至于为什么可以假设一复习完就考试呢?是因为只要时间总和相同,一个时间段内的事件是可以任意分配的,只要考试时间在这个时间段内,那么先考后考不影响结果。

代码如下:

#include <bits/stdc++.h>
 
using namespace std;
const int mx=100001;
struct cls //存储科目复习时间和考试时间
{
    int a,b;
}c[mx];
bool cmp(cls x,cls y)//用于sort排序
{
    return x.b<y.b;
}
int main()
{
    int n,ans=0,flag=0;
    cin >> n;
    for(int i=0;i<n;i++)
    {
        cin >> c[i].a;
    }
    for(int i=0;i<n;i++)
    {
        cin >> c[i].b;
    }
    sort(c,c+n,cmp);
    for(int i=0;i<n;i++)//直接贪心从最近考试的科目开始遍历
    {
        ans+=c[i].a;
        if(ans>c[i].b){flag++;break;}//复习不完就退出
        ans+=2;//加上考试时间
    }
    if(flag){cout << "NO" << endl;}
    else {cout << "YES" << endl;}
    return 0;
}
1. 用户与身体信息管理模块 用户信息管理: 注册登录:支持手机号 / 邮箱注册,密码加密存储,提供第三方快捷登录(模拟) 个人资料:记录基本信息(姓名、年龄、性别、身高、体重、职业) 健康目标:用户设置目标(如 “减重 5kg”“增肌”“维持健康”)及期望周期 身体状态跟踪: 体重记录:定期录入体重数据,生成体重变化曲线(折线图) 身体指标:记录 BMI(自动计算)、体脂率(可选)、基础代谢率(根据身高体重估算) 健康状况:用户可填写特殊情况(如糖尿病、过敏食物、素食偏好),系统据此调整推荐 2. 膳食记录与食物数据库模块 食物数据库: 基础信息:包含常见食物(如米饭、鸡蛋、牛肉)的名称、类别(主食 / 肉类 / 蔬菜等)、每份重量 养成分:记录每 100g 食物的热量(kcal)、蛋白质、脂肪、碳水化合物、维生素、矿物质含量 数据库维护:管理员可添加新食物、更新养数据,支持按名称 / 类别检索 膳食记录功能: 快速记录:用户选择食物、输入食用量(克 / 份),系统自动计算摄入的养成分 餐次分类:按早餐 / 午餐 / 晚餐 / 加餐分类记录,支持上传餐食照片(可选) 批量操作:提供常见套餐模板(如 “三明治 + 牛奶”),一键添加到记录 历史记录:按日期查看过往膳食记录,支持编辑 / 删除错误记录 3. 养分析模块 每日养摄入分析: 核心指标计算:统计当日摄入的总热量、蛋白质 / 脂肪 / 碳水化合物占比(按每日推荐量对比) 微量养素分析:检查维生素(如维生素 C、钙、铁)的摄入是否达标 平衡评估:生成 “养平衡度” 评分(0-100 分),指出摄入过剩或不足的养素 趋势分析: 周 / 月养趋势:用折线图展示近 7 天 / 30 天的热量、三大养素摄入变化 对比分析:将实际摄入与推荐量对比(如 “蛋白质摄入仅达到推荐量的 70%”) 目标达成率:针对健
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值