慕容琳的后花园
慕容琳的后花园
算法进阶#基本数据结构#C

T1-Supermarket

给定n个商品,每个商品有利润pi和过期时间di。每天只能卖一个商品,过期商品不能卖。求如何安排每天卖的商品可以使收益最大。

解法

容易想到贪心思路,每次买当前没过期的商品中价值最大的,但是因为从前往后买会影响到后来的状态,所以应该把时间从后往前扫
但是这样很难实现,我们换一种思路,设一个堆,堆中存储当前要买的,最优就是每天都买,堆的size就是买的天数,需要离散化
先将商品按照时间从小到大排序,对于每一个商品,如果它的购买时间已经过了,就跳过
如果购买时间刚好,那么就看当前堆顶与该商品的价值,因为是小根堆,所以这就是反悔措施
如果购买时间还没到,就直接推进去

ac代码

#include<bits/stdc++.h>
using namespace std;
struct node{int p,d;}s[10010];
struct heap
{
    int n,hp[10010];
    inline void up(int fa){while(fa>1)if(hp[fa]<hp[fa>>1])swap(hp[fa],hp[fa>>1]),fa=fa>>1;else break;return ;}
    inline void push(int v){hp[++n]=v,up(n);return;}
    inline void down(int fa)
    {int s=fa<<1;while(s<=n){if(s<n&&hp[s]>hp[s+1])s++;if(hp[s]<hp[fa])swap(hp[s],hp[fa]),fa=s,s=fa<<1;else break;}return;}
    inline void f(){hp[1]=hp[n--],down(1);}
    inline int top(){return hp[1];}
}a;
inline bool cmp(const node &a,const node &b){return a.d<b.d;}
int main()
{
    int n,x;
    while(cin>>n)
    {
        int ans=0;
        for(register int i=1;i<=n;i++)scanf("%d%d",&s[i].p,&s[i].d);
        sort(s+1,s+1+n,cmp);
        for(register int i=1;i<=n;i++)
            if(s[i].d==a.n){if(s[i].p>a.top())a.f(),a.push(s[i].p);}
            else if(s[i].d>a.n)a.push(s[i].p);
        for(register int i=1;i<=a.n;i++)ans+=a.hp[i],a.hp[i]=0;
        a.n=0,printf("%d\n",ans);
    }
    return 0;
}

T2-Sequence

给你m个序列,每次从每个序列里挑选一个数组成一个新的序列,每个序列进行加和组成一个新的序列,然后输出前n小的序列。

解法

一个很暴力的解法就是每次对于2个序列,求出n方个新序列,然后排序,这样的复杂度是mn^2logn^2
那么哪里可以优化呢,理想的复杂度是mnlogn,我们需要在求新序列时进行改动
先考虑2个序列,长度都为n,将他们从小到大排序,可以发现最小的一定是a[1]+b[1]
而次小的一定是a[1]+b[2]或者a[2]+b[1],那么我们可以确定,当某一个a[i]+b[j]为第k小时,a[i+1]+b[j]与a[i]+b[j+1]成为第k+1小的候选答案
利用这一个性质,我们就可以达到nlogn求解2个序列前n小的问题,那么总的复杂度就是mnlogn

ac代码

#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
int T,n,m,a[2010],b[2010],res[2010];
struct node
{
    int i,j,last;
    bool operator < (const node &x) const{return a[i]+b[j]>a[x.i]+b[x.j];}
};
priority_queue<node>q;
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++)scanf("%d",&b[i]);
        sort(b+1,b+1+m);
        for(int i=1;i<=m;i++)res[i]=b[i];
        for(int i=2;i<=n;i++)
        {
            for(int j=1;j<=m;j++)scanf("%d",&a[j]);
            sort(a+1,a+1+m);
            while(!q.empty())q.pop();
            q.push((node){1,1,0});
            for(int j=1;j<=m;j++)
            {
                node k=q.top();
                q.pop(),q.push((node){k.i,k.j+1,1});
                if(!k.last)q.push((node){k.i+1,k.j,0});
                res[j]=a[k.i]+b[k.j];
            }
            for(int j=1;j<=m;j++)b[j]=res[j];
        }
        for(int i=1;i<m;i++)printf("%d ",res[i]);
        printf("%d\n",res[m]);
    }
    return 0;
}

T3-数据备份

你在一家IT公司为大型写字楼或办公楼(offices)的计算机数据做备份。然而数据备份的工作是枯燥乏味的,因此你想设计一个系统让不同的办公楼彼此之间互相备份,而你则坐在家中尽享计算机游戏的乐趣。
已知办公楼都位于同一条街上。你决定给这些办公楼配对(两个一组)。每一对办公楼可以通过在这两个建筑物之间铺设网络电缆使得它们可以互相备份。
然而,网络电缆的费用很高。当地电信公司仅能为你提供K条网络电缆,这意味着你仅能为K对办公楼(或总计2K个办公楼)安排备份。任一个办公楼都属于唯一的配对组(换句话说,这2K个办公楼一定是相异的)。
此外,电信公司需按网络电缆的长度(公里数)收费。因而,你需要选择这K对办公楼使得电缆的总长度尽可能短。换句话说,你需要选择这K对办公楼,使得每一对办公楼之间的距离之和(总距离)尽可能小。

解法

易得贪心策略,每次取一定是取相邻的办公楼,那么可以转化问题,只需要取完差分数组,问题就变成n个数取k个,使这k个数的和最小且不相邻
那么这个问题怎么求解呢,有一个基础贪心策略是每次都取最小的,但很容易证明是错的,比如反例2 1 2 9,取了1之后就只能取9,但答案显然是取2 2
我们需要添加反悔措施,在此反例中,我们取完1后,需要删除左右的2,但不删除1,而是将其改成2+2-1=3,下一次就会取3,总的意义就是1+2+2-1,实际就是取了2 2
思路明白了,怎么用代码实现呢,我们用堆和双向链表维护,每次从堆中取出当前最小的,用链表删除其前驱与后继,并将其的值修改,总复杂度nlogn

ac代码

#include<bits/stdc++.h>
using namespace std;
struct P{int v,l,r;}p[100010];
struct node{int v,id;bool operator < (node it) const{return v>it.v;}}nw;
int n,m,ans,la,in;
bool vis[100010];
priority_queue<node>q;
void del(int x){p[x].l=p[p[x].l].l,p[x].r=p[p[x].r].r,p[p[x].l].r=x,p[p[x].r].l=x;}
int main()
{
    scanf("%d%d%d",&n,&m,&la);
    for(int i=1;i<n;++i)scanf("%d",&in),p[i].v=in-la,la=in,p[i].l=i-1,p[i].r=i+1,q.push((node){p[i].v,i});
    p[0].v=p[n].v=0x3f3f3f3f;
    for(int i=1;i<=m;++i)
    {
        while(vis[q.top().id])q.pop();
        nw=q.top(),q.pop(),ans+=nw.v,vis[p[nw.id].l]=vis[p[nw.id].r]=1;
        p[nw.id].v=p[p[nw.id].l].v+p[p[nw.id].r].v-p[nw.id].v;
        q.push((node){p[nw.id].v,nw.id}),del(nw.id);
    }
    printf("%d",ans);
    return 0;
}

T4-黑匣子

Black Box是一种原始的数据库。它可以存储一个整数数组,还有一个特别的变量i。最开始的时候Black Box是空的,而i等于0。这个Black Box要处理一串命令。
命令只有两种:
ADD(x):把x元素放进Black Box;
GET:i加1,然后输出Black Box中第i小的数。
记住:第i小的数,就是Black Box里的数按从小到大的顺序排序后的第i个元素。
现在要求找出对于给定的命令串的最好的处理方法。ADD和GET命令分别最多有200000个。
现在用两个整数数组来表示命令串:
1.A(1),A(2),…A(M):一串将要被放进Black Box的元素。每个数都是绝对不超过2000000000的整数,M≤200000。
2.u(1),u(2),…u(N):表示第u(j)个元素被放进了Black Box里后就出现了一个GET命令。

解法

之前讲过一道与这个差不多的题目,都是运用对顶堆,大根堆内存放从小到大前i个元素,小根堆内存放剩余元素
每次询问时只需要将元素都先压进小根堆中,再将小根堆堆顶压进大根堆中,保持大根堆元素数量,之后维护上述性质

ac代码

#include<bits/stdc++.h>
using namespace std;
int n,m,a[200010],q,nw=0;
priority_queue<int>qa;
priority_queue<int,vector<int>,greater<int> >qb;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&q);
        while(nw<q)qb.push(a[++nw]);
        qa.push(qb.top()),qb.pop();
        while(!qa.empty()&&!qb.empty()&&qa.top()>qb.top())
        {
            int x=qa.top();
            int y=qb.top();
            qa.pop(),qb.pop();
            qa.push(y),qb.push(x);
        }
        printf("%d\n",qa.top());
    }
    return 0;
}

T5-生日礼物

ftiasch 18岁生日的时候,lqp18_31给她看了一个神奇的序列A1,A2,…,AN.她被允许选择不超过M个连续的部分作为自己的生日礼物。ftiasch想要知道选择元素之和的最大值。你能帮助她吗?

解法

容易得到一个贪心,相同符号且相邻的一定可以合并,0可以省略,如果合并后正数数量不超过m,答案就是所有的正数
如果超过了m呢,我们可以把这个序列按照每个数的绝对值压进小根堆中,因为经过上述处理,该序列一定是正负相间
如果是负数,那么它两边的正数的绝对值一定比它的绝对值大,将两边的正数和这个负数合并,答案的损失是这个负数的绝对值
如果是正数,就选择不取这个正数,答案的损失是这个正数的绝对值,而不管是负数还是正数,都可以使取的正数的数量减少1
所以本质上这两种操作是一样的,但需要判断边界,边界上的正数无影响,但负数不可取,因为边界上的负数取了不会使正数的数量减少,但对答案有损失
至于代码实现,也是用堆和链表,链表来实现查找前驱与后继和删除元素,一直处理直至正数个数不超过m

ac代码

#include<bits/stdc++.h>
#define inf 0x7fffffff
using namespace std;
struct heap{int v,x,av;}h[200010],k;
int n,m,f[100010],tmp,sz,ans,tot,v[100010],cnt,next[100010],pre[100010],pos[100010],a,b;
void pushup(int x)
{
    while(h[x].av<h[x>>1].av)pos[h[x>>1].x]=x,swap(h[x],h[x>>1]),x>>=1;
    pos[h[x].x]=x;
}
void push(int v,int x){sz++,h[sz].v=v,h[sz].x=x,h[sz].av=abs(v),pos[x]=sz,pushup(sz);}
void pushdown(int x)
{
    int to;
    while((x<<1)<=sz)
    {
        to=(x<<1);
        if(to<sz&&h[to].av>h[to+1].av)to++;
        if(h[x].av>h[to].av)pos[h[to].x]=x,swap(h[to],h[x]),x=to;
        else break;
    }
    pos[h[x].x]=x;
}
void del(int x){h[x].av=inf,pushdown(x);}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){scanf("%d",&f[++tmp]);if(f[tmp]==0)tmp--;}
    v[++cnt]=f[1];
    for(int i=2;i<=tmp;i++)
        if((f[i]>0&&f[i-1]>0)||(f[i-1]<0&&f[i]<0))v[cnt]+=f[i];
        else v[++cnt]=f[i]; 
    for(int i=1;i<=cnt;i++)if(v[i]>0){ans+=v[i];tot++;}
    for(int i=1;i<=cnt;i++){next[i]=i+1;pre[i]=i-1;}
    pre[1]=-1,next[cnt]=-1;
    for(int i=1;i<=cnt;i++)push(v[i],i);
    while(tot>m)
    {
        k=h[1];
        if(pre[k.x]==-1)
        {
            if(k.v>0){ans-=k.v;del(1);}
            else{del(1);tot++;}
            pre[next[k.x]]=-1;
        }
        else if(next[k.x]==-1)
        {
            if(k.v>0){ans-=k.v;del(1);}
            else{del(1);tot++;}
            next[pre[k.x]]=-1;
        }
        else
        {
            ans-=k.av,a=next[k.x],b=pre[k.x];
            pre[k.x]=pre[b],next[pre[b]]=k.x;
            next[k.x]=next[a],pre[next[a]]=k.x;
            h[1].av=h[pos[a]].av+h[pos[b]].av-h[1].av;
            h[1].v+=h[pos[a]].v+h[pos[b]].v;
            pushdown(1),del(pos[a]),del(pos[b]);
        }
        tot--;
    }
    printf("%d",ans);
    return 0;
}

T6-合并果子

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

解法

这道题用贪心,显然,每次合并最小的两堆果子最优

ac代码

#include<bits/stdc++.h>
using namespace std;
int n,a[20000],ans=0;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    sort(a+1,a+1+n);
    int i=1,res;
    while(1)
    {
        res=a[i]+a[i+1];
        ans+=res;
        i++;
        a[i]=res;
        for(int j=i;j<n;j++)
            if(a[j]>a[j+1])swap(a[j],a[j+1]);
            else break;
        if(i==n)break;
    }
    printf("%d",ans);
    return 0;
}

T7-荷马史诗

追逐影子的人,自己就是影子。——荷马
Allison最近迷上了文学。她喜欢在一个慵懒的午后,细细地品上一杯卡布奇诺,静静地阅读她爱不释手的《荷马史诗》。但是由《奥德赛》和《伊利亚特》组成的鸿篇巨制《荷马史诗》实在是太长了,Allison想通过一种编码方式使得它变得短一些。
一部《荷马史诗》中有n种不同的单词,从1到n进行编号。其中第i种单词出现的总次数为wi。Allison想要用k进制串si来替换第i种单词,使得其满足如下要求:
对于任意的1≤i,j≤n,i≠j,都有:si不是sj的前缀。
现在Allison想要知道,如何选择si,才能使替换以后得到的新的《荷马史诗》长度最小。在确保总长度最小的情况下,Allison还想知道最长的si的最短长度是多少?
一个字符串被称为k进制字符串,当且仅当它的每个字符是0到k−1之间(包括0和k−1)的整数。
字符串Str1被称为字符串Str2的前缀,当且仅当:存在1≤t≤m,使得Str1=Str2[1..t]。其中,m是字符串Str2的长度Str2[1..t]表示Str2的前t个字符组成的字符串。

解法

这道题是个哈夫曼树,我们使出现次数多的单词离根节点最近,所以排序之后做一遍哈弗曼树即可

ac代码

#include<bits/stdc++.h>
using namespace std;
struct t{long long v,h;}a,need;
bool operator < (t x,t y)
{
    if(x.v!=y.v)return x.v>y.v;
    return x.h>y.h;
}
priority_queue<t>q;
long long top,ans,n,k,tmp,maxx;
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)scanf("%lld",&a.v),a.h=1,q.push(a);
    if((n-1)%(k-1)!=0)top+=k-1-(n-1)%(k-1);
    for(int i=1;i<=top;i++)need.v=0,need.h=1,q.push(need);
    top+=n;
    while(top!=1)
    {
        tmp=0,maxx=0;
        for(int i=1;i<=k;i++)a=q.top(),tmp+=a.v,maxx=max(maxx,a.h),q.pop();
        ans+=tmp,a.v=tmp,a.h=maxx+1,q.push(a),top-=k-1;
    }
    printf("%lld\n%lld",ans,q.top().h-1);
    return 0;
}

T8-双栈排序

Tom最近在研究一个有趣的排序问题。通过2个栈S1和S2,Tom希望借助以下4种操作实现将输入序列升序排序。
操作a
如果输入序列不为空,将第一个元素压入栈S1
操作b
如果栈S1不为空,将S1栈顶元素弹出至输出序列
操作c
如果输入序列不为空,将第一个元素压入栈S2
操作d
如果栈S2不为空,将S2栈顶元素弹出至输出序列
当然,这样的操作序列有可能有几个,Tom希望知道其中字典序最小的操作序列是什么。

解法

先考虑单栈排序,显然有结论:若存在一个k,使得i<j<k且a[k]<a[i]<a[j],那么a[i]和a[j]不能压入同一个栈
那么扩展到双栈排序,同一个栈即同一个二分图,那么我们做二分图的染色,对于不能在同一个栈的点连一条边,如果不是二分图,那就直接输出0
由于题目要求字典序最小,所以把编号小的放到第一个栈中,染完色后就模拟整个过程,因为要求字典序最小,所以先处理压进第一个栈的情况,如果出现冲突,就进行操作b,之后再进行操作c与操作d

ac代码

#include<bits/stdc++.h>
using namespace std;
int n,a[1010],f[1010],color[1010],cnt=1;
vector<int>e[1010];
stack<int>s1,s2;
queue<int>q;
void bfs(int s)
{
    while(!q.empty())q.pop();
    q.push(s),color[s]=1;
    while(!q.empty())
    {
        int now=q.front();
        for(int i=0;i<e[now].size();i++)
            if(color[e[now][i]]==-1)color[e[now][i]]=color[now]^1,q.push(e[now][i]);
            else if(color[e[now][i]]!=(color[now]^1))puts("0"),exit(0);
        q.pop();
    }
}
int main()
{
    memset(color,-1,sizeof(color)),scanf("%d",&n),f[n+1]=0x3f3f3f3f;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=n;i>=1;i--)f[i]=min(f[i+1],a[i]);
    for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)
        if(a[i]>f[j+1]&&a[i]<a[j])e[i].push_back(j),e[j].push_back(i);
    for(int i=1;i<=n;i++)if(color[i]==-1)bfs(i);
    for(int i=1;i<=n;i++)
    {
        if(color[i]==1)s1.push(a[i]),printf("a ");
        else s2.push(a[i]),printf("c ");
        while((!s1.empty() && s1.top()==cnt)||(!s2.empty()&&s2.top()==cnt))
        {
            if(!s1.empty()&&s1.top()==cnt)s1.pop(),printf("b ");
            else s2.pop(),printf("d ");
            cnt++;
        }
    }
    return 0;
}

T9-Subway tree systems

一些主要城市的地铁系统是以树的形式存在的,即在任何一对车站之间,只有一条地铁路线。此外,这些城市中的大多数都有一个独特的中央车站。
假设你是这些城市中的一个游客,你想探索所有的地铁系统。你从中央车站出发,随机选择一条地铁线路,然后跳上地铁车厢。每次你到达一个车站,你都会选择一条你还没有乘坐的地铁线路。如果在你现在的方向上没有其他车站可以探索,你可以之前乘坐的线路回去,直到中央车站。
之后,你对你的探索顺序的记忆就是你是离开中心站走得更远还是接近中心站,也就是说,你可以把你的旅行编码成一个二进制字符串,其中0表示远离中心站,1接近中央车站。

解法

这道题是树的最小表示法,2棵树同构显然是每个子树的节点数相同,所以我们可以从根节点开始,将每个子树的节点数求出并按同一狭义规则排序,即让一棵树排序结果相同
之后判断是否为同一棵树,具体实现见代码

ac代码

#include<bits/stdc++.h>
using namespace std;
int cnt[3010],cnt1[3010],pr[3010],len[2],i,T,tmp,node;
//cnt数组记录每个点的儿子个数,pr数组记录每个点的父亲节点
char str[2][3010];
bool cmp(int a,int b){return a>b;}
void gp(int x){node=0,memset(pr,-1,sizeof(pr));for(i=1;i<len[x];i++)if(str[x][i]=='0')pr[i]=node,node=i;else node=pr[node];}
//得到每个点的父亲节点
void gc(int x){for(i=0;i<len[x];i++){tmp=i;while(tmp!=-1){tmp=pr[tmp];if(tmp!=-1){if(!x)cnt[tmp]++;else cnt1[tmp]++;}}}}
//得到每个点的儿子个数
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s%s",str[0],str[1]),len[0]=strlen(str[0]),len[1]=strlen(str[1]);
        if(len[0]!=len[1]){printf("different\n");continue;}
        memset(cnt,0,sizeof(cnt)),memset(cnt1,0,sizeof(cnt1)),gp(0),gc(0),gp(1),gc(1);
        sort(cnt,cnt+len[0],cmp),sort(cnt1,cnt1+len[1],cmp);
        //按照儿子个数排序
        for(i=0;i<len[0];i++)if(cnt[i]!=cnt1[i])break;
        if(i==len[0])printf("same\n");else printf("different\n");
        //最后判断是否是同一棵树
    }
    return 0;
}

T10-滑窗

给你一个长度为N的数组,一个长为K滑动的窗体从最左移至最右端,你只能见到窗口的K个数,每次窗体向右移动一位
你的任务是找出窗口在各位置时的Max value,Min value。

解法

一道单调队列的裸题,分别维护两个升序和降序的单调队列

ac代码

#include <bits/stdc++.h>
using namespace std;
int n,k,a[1000010],q[1000010],l,r;
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    l=1,r=1,q[1]=1;
    for(int i=2;i<=k;i++)
    {
        while(a[i]<=a[q[r]]&&r>=l)r--;
        q[++r]=i;
    }
    printf("%d",a[q[l]]);
    for(int i=k+1;i<=n;i++)
    {
        if(q[l]+k==i)l++;
        while(a[i]<=a[q[r]]&&r>=l)r--;
        q[++r]=i;
        printf(" %d",a[q[l]]);
    }
    puts("");
    l=1,r=1,q[1]=1;
    for(int i=2;i<=k;i++)
    {
        while(a[i]>=a[q[r]]&&r>=l)r--;
        q[++r]=i;
    }
    printf("%d",a[q[l]]);
    for(int i=k+1;i<=n;i++)
    {
        if(q[l]+k==i)l++;
        while(a[i]>=a[q[r]]&&r>=l)r--;
        q[++r]=i;
        printf(" %d",a[q[l]]);
    }
    return 0;
}
赞赏
https://secure.gravatar.com/avatar/8cbc0c58deea66b18e9a96cd4bdd6eea?s=256&d=monsterid&r=g

慕容琳

文章作者

发表评论

textsms
account_circle
email

慕容琳的后花园

算法进阶#基本数据结构#C
T1-Supermarket 给定n个商品,每个商品有利润pi和过期时间di。每天只能卖一个商品,过期商品不能卖。求如何安排每天卖的商品可以使收益最大。 解法 容易想到贪心思路,每次买当前没过…
扫描二维码继续阅读
2019-05-06
功能