“当青训营遇上码上掘金” 后端主题题解

121 阅读3分钟

当青训营遇上码上掘金

主题 3:寻友之旅

小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)

请帮助小青通知小码,小青最快到达时间是多久?

输入: 两个整数 N 和 K  
输出: 小青到小码家所需的最短时间(以分钟为单位)

题目大意

求从N点到K点移动所需的最短时间,移动的方式有三种:x-1,x+1,2×\timesx

解题思路

本题很容易想到是最短路径的题目,因为我们可以把每一点都视为有3条边,且边的权重都为1

假设有一点x,那么它的边有以下三种:

  • x -> x-1
  • x -> x+1
  • x -> 2x

将问题转换之后,那么不就迎刃而解了

解法一:BFS

维护一个队列和一个vis数组,

  • 队列存储当前步数的所有节点,如从n点开始移动,

    那么走完第一步,即从n点将之前的三种移动方式全都走一遍,那么此时队列的情况为:{n-1,n+1,2n},

    走完第二步,即将当前队列的三个节点依次当作起点,再走完三种移动方式,那么此时队列的情况为:{n-2,n,2n-2,n,n+2,2n+2,2n-1,2n+1,4n}

  • vis数组记录已经走过的节点,防止重复走,极大降低了时间复杂度

    如刚刚队列的第二步的情况,其中第二个元素n,第三个元素n是绝对重复了的,因为n已经走过了

此种解法的时间复杂度为:O(|n-k|)

#include<iostream>  
#include<queue>  
#define maxn 100000  
using namespace std;  
typedef long long ll;  
struct NODE{  
    int i;  
    int dis;  
    NODE(int i,int dis):i(i),dis(dis){}  
};  
bool vis[maxn+1];  
queue<NODE> q;  
int n,k;  
int ans;  
inline char gc()  
{  
    static char buf[1 << 12], * p1 = buf, * p2 = buf;  
    return (p1 == p2) && (p2 = (p1 = buf) + fread(buf, 11 << 12, stdin), p1 == p2) ? EOF : *p1++;  
}  
inline ll read()  
{  
    ll x = 0, f = 1;  
    char ch(gc());  
    while (ch<'0' || ch>'9')  
    {  
        if (ch == '-')  
            f = -1;  
        ch = gc();  
    }  
    while (ch >= '0' && ch <= '9')  
    {  
        x = (x << 3) + (x << 1) + (ch ^ 48);  
        ch = gc();  
    }  
    return x * f;  
}  
int main()  
{  
    n=read(),k=read();  
    if(n>=k)  
    {  
        cout<<n-k;  
        return 0;  
    }  
    q.push({n,0});  
    vis[n]=1;  
    while(!q.empty())  
    {  
        NODE now=q.front();  
        q.pop();  
        if(now.i==k)  
        {  
            cout<<now.dis;  
            break;  
        }  
        int next[3]={now.i-1,now.i+1,now.i<<1};  
        for(int i=0;i<=2;i++)  
        {  
            if(!vis[next[i]]&&next[i]>=0&&next[i]<=maxn)  
            {  
                vis[next[i]]=1;  
                q.push({next[i],now.dis+1});  
            }  
        }  
    }  
    return 0;  
}

解法二:Dijkstra

维护一个优先队列q和一个dis数组

  • dis数组,存储到每一个点所需的最短距离

  • 优先队列q也是存储了当前步数的所有节点,但是出队的元素是当前队列里面的最短距离的元素

    因为本题中边的权重为1,其实图是一个有向无权图,所以解法一的队列和解法二的优先队列其实是差不多的

    每次出队时,看出队元素的节点到相邻节点(n-1,n+1,2n)的距离如果比之前已经求出的路径短,那么则更新这个相邻节点的距离,即更新dis数组,然后将相邻节点入队

此种解法的时间复杂度为:O(ElgV)=O(3(|n-k|)lg(|n-k|))

#include<iostream>  
#include<algorithm>  
#include<queue>  
#define maxn 100000  
#define inf 0x7fffffff  
using namespace std;  
typedef long long ll;  
struct NODE{  
    int i;  
    int dis;  
    NODE(int i,int dis):i(i),dis(dis){}  
    bool operator<(const NODE p)const{  
        return p.dis<dis;  
    }  
};  
priority_queue<NODE> q;  
int dis[maxn+1];  
int n,k;  
int cnt;  
inline char gc()  
{  
    static char buf[1<<12],*p1=buf,*p2=buf;  
    return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,1<<12,stdin),p1==p2)?EOF:*p1++;  
}  
inline int read()  
{  
    int x=0,f=1;  
    char ch(gc());  
    while(ch<'0'||ch>'9')  
    {  
        if(ch=='-')  
            f=-1;  
        ch=gc();  
    }  
    while(ch>='0'&&ch<='9')  
    {  
        x=(x<<3)+(x<<1)+(ch^48);  
        ch=gc();  
    }  
    return x*f;  
}  
void dijkstra(int s)  
{  
    for(int i=0;i<=maxn;i++)  
        dis[i]=inf;  
    dis[s]=0;  
    q.push({s,0});  
    while(!q.empty())  
    {  
        NODE now=q.top();  
        q.pop();  
        int u=now.i;  
        // 划重点!!!  不加可能会超时!  
        // 如果dis数组中已更新的最短路径  
        // 比当前点最路路径的长度要短  
        // 那么从当前点发出的所有边都是没用的  
        if(dis[u]<now.dis)  
            continue;  
        int next[3]={u-1,u+1,u<<1};  
        for(int i=0;i<=2;i++)  
        {  
            if(next[i]<0||next[i]>maxn)  
                continue;  
            if(dis[next[i]]>dis[u]+1)  
            {  
                dis[next[i]]=dis[u]+1;  
                q.push({next[i],dis[next[i]]});  
            }  
        }  
    }  
}  
int main()  
{  
    n=read(),k=read();  
    if(n>=k)  
        cout<<n-k;  
    else  
    {  
        dijkstra(n);  
        cout << dis[k] << " ";  
    }  
    return 0;  
}

主题 4:攒青豆

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

攒青豆.png

以下为上图例子的解析:

输入:height = [5,0,2,1,4,0,1,0,3]  
输出:17  
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

解题思路

由样例图可以看出,我们其实可以将图分为两个区域:5,0,2,1,4和4,0,1,0,3

每一个区域的最左边是当前区域最高的柱子,每一个区域的最右边是当前区域的最高柱子,

那么青豆的高度就是每一个区域第二高柱子的高度,

青豆的面积就是青豆减去每一个区域中间柱子的高度(除开区域两边的柱子)的和

通过思索后我们又可以将问题转化为求出给定序列的全部连续下降的子序列,

如样例中的连续下降子序列有:5,2,1和4,1

求出连续下降子序列后,那么每一个区域的划分就是每一个子序列的第一个元素和子序列最后一个元素的下一个元素

时间复杂度为:O(n)

#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
typedef long long ll;

inline char gc()
{
	static char buf[1 << 12], * p1 = buf, * p2 = buf;
	return (p1 == p2) && (p2 = (p1 = buf) + fread(buf, 1, 1 << 12, stdin), p1 == p2) ? EOF : *p1++;
}
inline ll read()
{
	ll x = 0, f = 1;
	char ch(gc());
	while (ch<'0' || ch>'9')
	{
		if (ch == '-')
			f = -1;
		ch = gc();
	}
	while (ch >= '0' && ch <= '9')
	{
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = gc();
	}
	return x * f;
}
ll n;
ll a[100000];
ll maxa,last,ans;
queue<int> q;
int main()
{
	ll n=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	if(n<=2)
		// 柱子小于等于2个肯定攒不了豆子了~
		cout<<0;
	maxa=a[1]; // 记录区间最高的柱子高度
	last=a[1]; // 记录区间上一个的柱子高度
	for(int i=2;i<=n;i++)
	{
		if(a[i]==0)
		{
			// 没有柱子直接入队
			q.push(0);
		}
		else if(a[i]<=last)
		{
			// 当前柱子比上一个柱子矮,满足区间连续下降的特性
			// 继续入队
			q.push(a[i]);
			last=a[i];
		}
		else
		{
			// 当前柱子比上一个柱子高,不满足区间连续下降的特性
			// 结算豆子
			ll ma=min(maxa,a[i]);
			while(!q.empty())
			{
				ans+=ma-q.front();
				q.pop();
			}
			maxa=a[i]; // 记录下一个区间的最高的柱子高度
		}
	}
	cout<<ans;
	return 0;
}