基础算法(算法进阶指南)

目录

一,位运算

1,起床困难综合症

二,递归与递推

1,费解的开关

2,约数之和

三,前缀和与差分

1,最高的牛

四,二分

1,最佳牛围栏

五,对顶堆

1,动态中位数

六,逆序对

1,超快速排序

2,奇数码问题

七,贪心

1.股票买卖II

2,防晒

3.畜栏预定

4,雷达设备

5,国王游戏

6,给树染色

八,习题

1,士兵

2,赶牛入圈

3,防线

4,任务


一,位运算

1,起床困难综合症

题目链接:https://www.acwing.com/problem/content/1000/

题目有很多废话,这里就不复制题目过来了,需要的看上面链接,题目大意就是,在0~m中选择一个数,经过n次按位与,按位或,按位异或的操作的值最大,输出经过n次运算后的值,所以我们并不用考虑初始的值,只需要判断一下边界条件,在我们选的值要保证小于m就可以了,那这题我们该怎么去分析呢,位运算有个性质,就是对于每个操作,每一位的变化都是独立的,不会影响其他位的结果

所以我们可以用一个初始全为0,和全为1的二进制数,进行n次运算,最后通过分析这两个数来确定我们的答案,如何确定呢,对于某一位,首先我们要考虑他是否可以由初始为0的情况最终变成1,如果可以的话我们就可以就让初始值的这一位为0,这一位最终能变成1,那么对答案的贡献就是1<<I(i表示这一位的位置),如果不能由初始为0的情况最终变成1,那么我们再考虑这一位是否能由初始为1的情况变过来,如果可以的话我们还要判断一下边界条件,就是这个数是否小于m,如果满足的话我们就可以让这一位初始为1,最终能得到1,那么对答案的贡献就是1<<i(i表示这一位的位置)

还有两种情况我们是不用考虑的,就是初始0变0和1变0,这两种情况对我们的答案都不会有变大的帮助,所以就不用管这两种情况了

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

int main()
{
    int none=0,one=-1;//用初始全0的二进制和初始全1的二进制数去进行n次运算
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
    {
        char op[5];
        int x;
        scanf("%s%d",op,&x);
        if(op[0]=='A')
            none&=x,one&=x;
        else if(op[0]=='O')
            none|=x,one|=x;
        else 
            none^=x,one^=x;
    }
    int res=0;
    for(int i=29;i>=0;i--)
    {
         //注意这里有个优先级的关系,如果这一位经过n次运算后能变成1,如果这个1既能从初始的1和初始的0变过来
         //那么我们优先选取0变过来的情况,因为这样不用考虑初始的数是否小于m
        if(none>>i&1)res+=1<<i; 
        else if(one>>i&1&&(1<<i)<=m)//所以不能是两个if,而是if和else if,先看能不能从0变过来,不能再看1
        {
            res+=(1<<i);
            m-=(1<<i);//如果这一位的初始值选了1,那么之后的数就要小于m-=(1<<j)
            //例如给的m是1000,如果我们最高位选了1,如果没有这一步,那么下一位当1满足条件的时候我们还能选1
            //但事实上是不能再选的
        }
    }
    cout<<res;
    return 0;
}

二,递归与递推

1,费解的开关

题目链接:https://www.acwing.com/problem/content/description/97/

这个题,我们会发现有下面两个性质,操作的顺序是随便的,一个位置只能按一次开关,因为一个开关按两次等于没按,所以要想求最小步数,我们一个方格只能按一次

假设我们对第一行的任意方格进行操作,相邻的方格都会改变状态,所以在我们对一行操作完以后,如果有关闭的灯 ,只能通过第二行的同一列的开关将暗的灯改变成开的灯,因此我们对一行的操作会直接影响到下一行的操作,即如果第一行的操作确定以后,整个方格的操作就已经确定了,那么我们这个时候就要考虑如何确定第一行的操作,得到一个最小的合法步数,因为这一题的数据范围比较小,每一行只有5个方格,对于每一个方格,我们有2种选择,开与不开,所以总共有2^5种选择,只要暴力枚举所有情况就可以得到一个最小的操作步数

代码如下:

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

const int N=6;

char a[N][N],backup[N][N];

int dx[5]={0,0,1,0,-1};
int dy[5]={0,1,0,-1,0};
void turn(int x,int y)//定义一个偏移量,将上下左右四个方向包括自己都改变灯的状态
{
    for(int i=0;i<5;i++)
    {
        int tx=x+dx[i],ty=y+dy[i];
        if(tx<0||tx>=5||ty<0||ty>=5)
            continue;
        a[tx][ty]^=1;//这里有个小技巧,虽然a存储的字符,但是0的ascll码是48,二进制表示为110000
                    //1是49,二进制表示为110001,只有最后一位不同,所以可以用异或简化我们的代码
    }
}
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int res=10;//随便定义一个大于6的数,后面只要res还是大于6说明没有合法的方案
        for(int i=0;i<5;i++)
            cin>>backup[i];//以字符串输入
        for(int op=0;op<(1<<5);op++)//枚举第一行的所有操作
        {
            int step=0;
            memcpy(a,backup,sizeof backup);//每次操作都要复制一个原数组,在复制的数组中进行操作,因为每一次操作都代表一次方案,每个方案是独立,互不影响的
            for(int i=0;i<5;i++)
            {
                if(op>>i&1)//判断对第一行的操作了哪几个开关,1表示操作,0表示不操作
                {
                    turn(0,i);//将该灯的上下左右自己都改变灯的状态
                    step++;//操作次数加1
                }
            }
            for(int i=0;i<4;i++)//枚举前四行
            {
                for(int j=0;j<5;j++)//枚举每一列
                {
                    if(a[i][j]=='0')//如果这个位置为关灯状态
                    {
                        step++;//操作次数加1
                        turn(i+1,j);//用该行的下一行同一列的灯把这个变亮
                    }
                }
            }
            bool flag=true;
            for(int i=0;i<5;i++)//最后判断一个最后一行的灯是否全亮
            {
                if(a[4][i]=='0')//如果有不亮的,说明这个方案不合法
                {
                    flag=false;
                    break;
                }
            }
            if(flag)//方案合法更新最小步数
                res=min(step,res);
        }
        if(res>6)res=-1;//大于改成-1
        cout<<res<<endl;
    }
    return 0;
}

2,约数之和

题目链接:https://www.acwing.com/problem/content/description/99/

对于这题,首先我们要了解一个数的约数怎么求,不懂得可以参考我的另一篇博客

https://blog.csdn.net/m0_74911187/article/details/131826126?spm=1001.2014.3001.5501

这题得数据范围很大,所以不可以先求出a^b再求约数,由约数之和得公式我们可以得到

 然后我们可以用分治得思想求每个括号中得值,就有

代码如下:

#include<iostream>
#include<algorithm>
#include<map>
#include<cstdio>

using namespace std;
typedef long long ll;
map<int,int>mp;
int mod=9901;

int quick_mi(int a,int b)//快速幂
{
    int res=1;
    while(b)
    {
        if(b&1)
            res=(ll)res*a%mod;
        b>>=1;
        a=(ll)a*a%mod;
    }
    return res;
}
int sum(int p,int c)
{
    if(!c)return 1;
    if(c&1)
        return (1+quick_mi(p,(c+1)/2))*sum(p,(c-1)/2)%mod;
    else
        return ((1+quick_mi(p,c/2))*sum(p,c/2)-quick_mi(p,c/2))%mod;
}
int main()
{
    int a,b,res=0;
    cin>>a>>b;
    if(!a)res=0;
    else res=1;
    for(int i=2;i<=a/i;i++)//分解质因数
    {
        while(a%i==0)
        {
            mp[i]++;
            a/=i;
        }
    }
    if(a>1)
        mp[a]++;
    map<int,int>::iterator it;
    for(it=mp.begin();it!=mp.end();it++)
    {
        int p=(*it).first,c=(*it).second*b;
        res=res*sum(p,c)%mod;
    }
    cout<<res;
}

三,前缀和与差分

1,最高的牛

题目链接:https://www.acwing.com/problem/content/description/103/

这题用到了一个差分的思想,我们可以首先将所有牛的高度都看成0,如果a能看到b,那么就将【a+1,b-1】这个区间的数都减去1,对于区间上的操作,我们就可以用差分的思想用一个差分数组,这样就可以将区间修改变成单点修改,最后求一遍差分数组的前缀和,再每个值加个最高的那头牛的值,就是每头牛的高度

代码如下:

#include<iostream>
#include<algorithm>
#include<map>
using namespace std;

typedef pair<int,int>pii;

const int N=1e4+10;

map<pii,bool>mp;
int s[N];

int main()
{
    int n,p,h,m;
    cin>>n>>p>>h>>m;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        if(a>b)swap(a,b);
        if(mp[{a,b}])continue;//因为可能会重复出现,多一个判断,避免重复计算
        s[a+1]--;
        s[b]++;
        mp[{a,b}]=true;
    }
    for(int i=1;i<=n;i++)
    {
        s[i]+=s[i-1];
        cout<<s[i]+h<<endl;
    }
    return 0;
}

四,二分

1,最佳牛围栏

题目链接:https://www.acwing.com/problem/content/description/104/

这题题目要求大概就是在一段不小于给定长度L的区间找到平均值最大得那一段区间,可以用两层for循环来做,但是那样肯定会超时,我们来思考如何优化

二分答案,判定“是否存在一个长度不小于L的子段,平均数不小于二分的值”。如果把数列中的每个数都减去二分的值,就转化为判定“是否存在一个长度不小于L的子段,子段和非负”。

求一个长度不小于L,求和飞负的子段,我们可以用到双指针与前缀和,求了前缀和以后我们就可以快速的求一段区间的和,再用一个指针i=0,和j=L开始往后遍历,每往后走一步,只有一个值进入我们的区间,那么只要在前缀的区间中找到值最小的那个,就可以得到最大的区间和

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5+10;

double s[N],a[N],maxv,minv,l,r;
int n,m;

bool check(double x)
{
    for(int i=1;i<=n;i++)
        a[i]=a[i-1]+s[i]-x;//求一遍前缀和用于求各区间得平均值
    maxv=-1e9,minv=1e9;
    for(int i=0,j=m;j<=n;j++,i++)
    {
        minv=min(minv,a[i]);//minv记录前缀区间中值最小的那个数
        maxv=max(maxv,a[j]-minv);//maxv记录最大的区间和
        if(maxv>=0)//表示至少存在一个区间平均值大于x,可以继续二分,让平均值更大
            return true;
    }
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%lf",&s[i]);
        r=max(r,s[i]);//二分得初始右边界
    }
    while(r-l>1e-5)//实数二分
    {
        double mid=(l+r)/2;//这里是实数二分,所以不能用位运算操作符
        if(check(mid))
            l=mid;
        else
            r=mid;
    }
    cout<<(int)(r*1000);//一定是r,不能用l,因为l比平均值小
    return 0;
}

五,对顶堆

1,动态中位数

题目链接:https://www.acwing.com/problem/content/108/

题目大意就是动态打印中位数,对于这一类的问题我们通常都是用对顶堆来做,下面介绍一下对顶堆,对顶堆就是开两个堆,一个大根堆一个小根堆,小根堆在大根堆上面,也就是小根堆的值一定比大根堆大,我们把小于中位数的数放到大根堆中,大于中位数的数放到小根堆中,所以当大根堆中的数比小根堆的数多1时,大根堆的堆顶就是中位数

我们只要维护这两个堆,对于每一个新加入的x,如果大于中位数就放到小根堆,否则放到小根堆,当大根堆中的数量比小根堆中的数量多2时,就把大根堆的堆顶放到小根堆去,当小根堆中的数量比大根堆多1时,就把小根堆的堆顶放到大根堆中去,对顶堆的结构如下:

 代码如下:

#include<iostream>
#include<algorithm>
#include<queue>

using namespace std;

int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n,m,cnt=0;
        priority_queue<int>down;//大根堆
        priority_queue<int,vector<int>,greater<int>>up;//小根堆
        scanf("%d%d",&n,&m);
        printf("%d %d\n",n,(m+1)/2);
        for(int i=1;i<=m;i++)
        {
            int x;
            scanf("%d",&x);
            if(down.empty()||x<down.top())down.push(x);//如果这个数小于大根堆的堆顶,就加到大根堆去
            else up.push(x);//否则加到小根堆去
            
            //如果大根堆的数比小根堆的数多二,就把大根堆的堆顶加到小根堆去
            if(down.size()>up.size()+1)up.push(down.top()),down.pop();
            else if(up.size()>down.size())down.push(up.top()),up.pop();
            //如果小根堆的数比大根堆的数多1,就把小根堆的堆顶加到大根堆去
            
            if(i%2)
            {
                printf("%d ",down.top());//大根堆的堆顶就是中位数
                cnt++;
                if(cnt%10==0)
                    puts(" ");
            }
        }
         if(cnt%10)
             puts(" ");
    }
    return 0;
}

六,逆序对

1,超快速排序

题目链接;https://www.acwing.com/problem/content/109/

题目抽象出来就是求逆序对的数量,求逆序对最常用的方法就是归并排序的过程冲求逆序对的数量

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

typedef long long ll;

const int N=500010;

int a[N],tmp[N];

ll merge_sort(int l,int r)
{
    if(l>=r)return 0;
    
    int mid=l+r>>1;
    ll res=merge_sort(l,mid)+merge_sort(mid+1,r);//答案可能会爆int,用long long 存答案
    
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r)
    {
        if(a[i]<=a[j])
            tmp[k++]=a[i++];
        else
        {
            tmp[k++]=a[j++];
            res+=(ll)mid-i+1;//a[i]到a[mid]都大于a[j]
        }
    }
    while(i<=mid)
        tmp[k++]=a[i++];
    while(j<=r)
        tmp[k++]=a[j++];
    for(int i=l,j=0;i<=r;i++,j++)
        a[i]=tmp[j];//排好序的数组要还原数组
    return res;
}
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        if(n==0)break;
        for(int i=0;i<n;i++)
            scanf("%d",&a[i]);
        printf("%lld\n",merge_sort(0,n-1));
    }
    return 0;
}

2,奇数码问题

题目链接:https://www.acwing.com/problem/content/110/

证明比较复杂,这里直接给出结论,记住即可,奇数码游戏两个局面可达,当且仅当两个局面下网格中的数依次写成1行n*n-1个元素的序列(不考虑空格),逆序对个数的奇偶性相同。

上面的结论还可以扩展到n为偶数的情况,此时两个局面可达,当且仅当两个局面对应的网格写成序列后,“逆序对数之差”和“两个局面下空格所在行数之差“奇偶性相同

求逆序对同样用到归并排序,与上一题代码类似,代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=510*510;

int a[N],b[N];
int tmp[N];
int merge_sort(int c[],int l,int r)
{
    if(l>=r)return 0;
    
    int mid=l+r>>1;
    int res=merge_sort(c,l,mid)+merge_sort(c,mid+1,r);
    
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r)
    {
        if(c[i]<=c[j])tmp[k++]=c[i++];
        else
        {
            tmp[k++]=c[j++];
            res+=mid-i+1;
        }
    }
    while(i<=mid)tmp[k++]=c[i++];
    while(j<=r)tmp[k++]=c[j++];
    
    for(int i=l,j=0;i<=r;i++,j++)
        c[i]=tmp[j];
    return res;
}
int main()
{
    int n;
    while(cin>>n)
    {
        int k=0;
        for(int i=0;i<n*n;i++)
        {
            int x;
            cin>>x;
            if(x!=0)a[k++]=x;
        }
        int res1=merge_sort(a,0,n*n-2);
        k=0;
        for(int i=0;i<n*n;i++)
        {
            int x;
            cin>>x;
            if(x!=0)b[k++]=x;
        }
        int res2=merge_sort(b,0,n*n-2);
        if((res1&1&&res2&1)||(res1%2==0&&res2%2==0))
            printf("TAK\n");
        else
            printf("NIE\n");
    }
    return 0;
}

七,贪心

1.股票买卖II

题目链接:https://www.acwing.com/problem/content/1057/

这题我们用到的时贪心的思路,只要我们股票涨了,我们就卖,这样就可以得到一个最优解

如何证明呢?对于一段股票价格连续上升的情况,我们可以将其拆分成很多个小段,不断重复买与卖,与我们一次性买入和卖出,得到的答案是一样的

代码如下:

#include<iostream>
#include<algorithm>
#include<cstdio>

using namespace std;

const int N=1e5+10;

int a[N];

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&a[i]);
    int res=0;
    for(int i=1;i<n;i++)
    {
        int p=a[i]-a[i-1];
        if(p>0)
            res+=p;
    }
    printf("%d",res);
    return 0;
}

2,防晒

题目链接:https://www.acwing.com/problem/content/description/112/

这题同样是用贪心的思路,我们用牛的最大承受阳光程度来排序,防晒的等级来排序,就可以得到最优解

代码如下:

#include<iostream>
#include<algorithm>

#define x first
#define y second
using namespace std;

typedef pair<int,int>pii;

const int N=2510;

pii cow[N],bottle[N];

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
    {
        int minv,maxv;
        scanf("%d%d",&minv,&maxv);
        cow[i]={maxv,minv};
    }
    for(int i=0;i<m;i++)
    {
        int x1,number;
        scanf("%d%d",&x1,&number);
        bottle[i]={x1,number};
    }
    sort(cow,cow+n);
    sort(bottle,bottle+m);
    int res=0;
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            auto p=bottle[j];
            int x1=p.x,number=p.y;
            if(x1>=cow[i].y&&x1<=cow[i].x&&number)
            {
                res++;
                bottle[j].y--;
                break;
            }
        }
    }
    printf("%d",res);
    return 0;
}

3.畜栏预定

题目链接:https://www.acwing.com/problem/content/description/113/

题解思路:按照开始吃草的时间将牛排序,用一个优先队列记录当前每个畜栏安排进去的最后一头牛,最初没有畜栏,依次对每头牛扫描优先队列,尝试把当前的牛安排在堆顶的畜栏,如果安排不了的话就新建一个畜栏

代码如下:

#include<iostream>
#include<algorithm>
#include<queue>
#include<cstdio>
#define x first
#define y second

using namespace std;

const int N=50010;

typedef pair<int,int>pii;

pair<pii,int>cow[N];//一维存储奶牛的吃草时间,二维记录奶牛的编号
int id[N];//id记录第i头奶牛属于哪条畜栏

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d%d",&cow[i].x.x,&cow[i].x.y);
        cow[i].y=i+1;//给奶牛编号
    }
    sort(cow,cow+n);
    priority_queue<pii,vector<pii>,greater<pii> >heap;//用一个优先队列存储每个畜栏的吃草结束时间
    for(int i=0;i<n;i++)
    {
        if(heap.empty()||cow[i].x.x<=heap.top().x)//没有可以加入的畜栏
        {
            id[cow[i].y]=heap.size()+1;//记录第i头奶牛属于哪个畜栏
            heap.push({cow[i].x.y,heap.size()+1});
        }
        else if(cow[i].x.x>heap.top().x)//有可以加入的畜栏
        {
            pii p=heap.top();
            heap.pop();
            id[cow[i].y]=p.y;
            heap.push({cow[i].x.y,p.y});
        }
    }
    printf("%d\n",heap.size());
    for(int i=1;i<=n;i++)
        printf("%d\n",id[i]);
    return 0;
}

4,雷达设备

题目链接:https://www.acwing.com/problem/content/description/114/

题解思路:对于x轴上方的每个小岛,可以计算x轴上一段能管辖他的区间l[i]~r[i]。问题就转化为:给定N个区间,在x轴放置最少的点,使每个区间包含至少一个点,按照每个区间的左端点l[i]从小到大排序,用一个变量维护已经安置的最后一台雷达的坐标last,起初last为负无穷,依次考虑每个区间。如果当前区间i的左端点l[i]大于最后一台雷达的坐标last,则新增一台雷达,令last=r[i]。否则就让最后一台已经安置好的雷达来管辖当前区间,并令last=min(last,r[i]),防止出现这种情况

 代码如下:

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;

typedef pair<double, double>pii;

const int N = 1010;

pii seg[N];

int main()
{
    int n, d;
    scanf("%d%d", &n, &d);
    bool flag = false;
    for (int i = 0; i < n; i++)
    {
        int x, y;
        cin >> x >> y;
        if (y > d)
            flag = true;
        double len = sqrt(d * d - y * y);
        seg[i] = { x - len,x + len };//根据小岛的位置求出雷达可放置的区间
    }
    if (flag)
        cout << "-1";
    else
    {
        sort(seg, seg + n);//按左端点排序
        double last = -1e9;
        int cnt = 0;
        for (int i = 0; i < n; i++)
        {
            if(seg[i].first>last)//如果当前区间的左端点大于最后一个雷达的位置,需要新建一个雷达
            {
                cnt++;
                last=seg[i].second;//雷达建在当前区间的右端点
            }
            else
                last=min(last,seg[i].second);
            //防止出现第二个区间的左端点大于第一个区间,右端点小于第二个区间
        }
        cout << cnt;
    }

    return 0;
}

5,国王游戏

题目链接:https://www.acwing.com/problem/content/description/116/

这题是一个典型的贪心,先给出思路,直接将所有大臣按左右手上的数的乘积从小到大排序,得到的序列就是最优排队方案。

证明与耍杂技的牛那题是一样的,假设交换最后一个大臣和不交换对比答案,我们会得到,排好序的情况得到的答案一定是最优解

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>
#include<cstdio>

#define x first
#define y second

using namespace std;

const int N=1010;

typedef pair<int,int>pii;
typedef vector<int>ver;

int n,a0,b0;
pii a[N];

ver mul(ver v,int x)//高精度乘法
{
    reverse(v.begin(),v.end());
    for(int i=0;i<v.size();i++)
        v[i]*=x;
    for(int i=0;i<v.size();i++)
    {
        if(v[i]<10)continue;
        int t =v[i]/10;
        v[i]%=10;
        if(i<v.size()-1)
            v[i+1]+=t;
        else
            v.push_back(t);
    }
    reverse(v.begin(),v.end());
    return v;
}

ver div(ver v,int x)//高精度除法
{
    ver b;
    bool flag=false;
    int s=0;
    for(int i=0;i<v.size();i++)
    {
        s=s*10+v[i];
        if(flag||s>=x)
        {
            flag=true;
            b.push_back(s/x);
        }
        s%=x;
    }
    return b;
}

ver max_1(ver maxv,ver v)//每次记录最大值
{
    if(maxv.size()==v.size())
        return maxv>v?maxv:v;
    return maxv.size()>v.size()?maxv:v;
}

int main()
{
    scanf("%d%d%d",&n,&a0,&b0);
    ver v,maxv;
    
    for(int i=0;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        a[i]={x*y,y};//以大臣左右手的金币数的乘积排序
    }
    sort(a,a+n);

    v.push_back(a0);//首先把国王左手的金币加进去
    maxv.push_back(1);//记录拿到最大金币数
    
    for(int i=0;i<n;i++)
    {
        if(i)
            v=mul(v,a[i-1].x/a[i-1].y);
        maxv=max_1(maxv,div(v,a[i].y));//注意,除的结果不用保存下来,只需要我们知道除了以后的答案就可以了
    }
    
    for(int i=0;i<maxv.size();i++)//输出答案
        printf("%d",maxv[i]);
    return 0;
}

6,给树染色

题目链接:https://www.acwing.com/problem/content/description/117/

这题同样是用贪心的思想,正常考虑的话,我们会认为从根节点往下找,每次给权值最大的那个点染色,这样就可以得到最优解,但事实上并不是,我们很容易找到一个反例,也就是当一个节点的有一个权值比较小的左儿子节点,这个左儿子的儿子节点非常多,并且有一个权值较大的右儿子节点,这个右儿子节点没有儿子点,按照上述思路我们会先给右儿子染色,这样就不能得到最优解了

虽然上述思路是错误的,但是我们能从中得到一个正确的信息,那就是:树中除了根节点以外,权值最大的那个节点一定会在它的父节点被染色后立即染色。

于是我们可以确定的是,树中权值最大的点及其父节点的染色操作是连续进行的,我们可以把这两个点合并起来,合并得到的点的新权值设为这两个点的权值的平均值(后面会证明为什么这么设)

 例如有权值为x,y,z的三个点,我们已知x和y的染色操作是连续进行的,那么就有两种可能的染色方案:1)先染x,y,再染z,代价是x+2y+3z。2)先染z,再染x和y,代价是z+2x+3y

设两式相减小于0,得z<(x+y)/2,也就是说,当x+y得平均值大于z时,第一种情况得代价会比较小,所以说我们要设两个点合并得权值为两个点的平均值,那么合并的代价就为父节点内的元素个数乘上该节点的权值总和,因为将该节点合并到父节点以后只有将父节点内的点全部染色完才能给该节点染色,那么每个节点的权值都要乘上一个偏移量,也就是父节点内的元素个数,每次合并要更新父节点内的信息

代码如下:

#include<iostream>
#include<algorithm>
#include<cstdio>

using namespace std;

const int N=1010;

//每个节点记录几个信息,分别是该节点的父节点,该节点里面的元素个数,该节点的权值总和,该节点的平均值
struct NODE
{
    int father,size,sum;
    double ave;
}node[N];
int n,root;

int find()//找到平均权值最大的那个节点,与他的父节点合并
{
    int u=0;
    double ave=0;
    for(int i=1;i<=n;i++)
    {
        if(i!=root&&node[i].ave>ave)//不能是根节点,因为根节点没有父节点
        {
            ave=node[i].ave;
            u=i;
        }
    }
    return u;//返回平均权值最大的那个节点的编号
}
int main()
{
    int ans=0;
    scanf("%d%d",&n,&root);
    for(int i=1;i<=n;i++)//读入信息
    {
        scanf("%d",&node[i].sum);
        node[i].ave=node[i].sum;//初始时每个节点的平均值都等于自身的权值,因为只有一个点
        node[i].size=1;
        ans+=node[i].sum;//答案首先要加上每个节点的权值,因为后面将节点与父节点合并时并不会算上父节点的权值
    }
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        node[b].father=a;//表示b的父节点是a
    }
    for(int i=1;i<=n;i++)
    {
        int u=find();//找到平均值最大的那个节点
        int father=node[u].father;//记录平均值最大的那个节点的父节点,用于后面合并时更新信息
        ans+=node[father].size*node[u].sum;//记录答案,每次合并时的权值
        for(int j=1;j<=n;j++)//从这一行往后都是合并的过程
            if(node[j].father==u)
                node[j].father=father;//将这个节点的儿子节点都指向该节点的父节点
        node[father].size+=node[u].size;//更新父节点内的节点个数
        node[father].sum+=node[u].sum;//更新父节点内的权值总和
        node[father].ave=(double)node[father].sum/node[father].size;//更新父节点的平均权值
        node[u].ave=-1;//等价于删除该节点
    }
    printf("%d",ans);
    return 0;
}

八,习题

1,士兵

题目链接:https://www.acwing.com/problem/content/description/125/

这题考察了贪心

对于这个题,我们会发现移动x和移动y是独立开来的,也就是说x不论怎么移动都不会影响y的移动,那么我们就可以把这个问题拆分成两个问题来看

对于y来说,与货仓选址这个题类似,只需要找到纵坐标的中位数就可以确保我们的纵向移动距离是最小的

对于x来说,要想步数最小,每个士兵移动前和移动后的相对位置一定是不变的,也就是说移动前1号士兵在2号士兵前面,移动后也一定是1号士兵在2号士兵前面,所以我们可以先对士兵进行一个排序,然后我们想将n个士兵紧挨着放在一条直线上,这里假设这条直线为a,a+1,a+2,……a+n,那么所有的距离总和就是|x1-a|+|x2-(a+1)|+|x3-(a+2)|+……|xn-(a+n)|,转换一下,就变成了|x1-a|+|(x2-1)-a)|+|(x3-2)-a|+……|(xn-n)-a|,也就是说在每个点减去一个等差数列得到的值后找到一个点,每个点到找到得这个点得距离总和最小,很显然这个点也是中位数

代码如下:

#include<iostream>
#include<algorithm>
#include<cmath>

using namespace std;

const int N=10010;

int x[N],y[N];
int n;
int work(int a[])//求每个点到中位数的距离
{
    sort(a,a+n);//先排序方便我们找中位数,排序后中间那个值就是中位数
    int res=0;
    for(int i=0;i<n;i++)
        res+=abs(a[i]-a[n/2]);
    return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d%d",&x[i],&y[i]);
    sort(x,x+n);//先排序
    for(int i=0;i<n;i++)x[i]-=i;//这里可以任何值,只要这个值满足等差数列即可,这里为了方便直接选i
    printf("%d",work(x)+work(y));
    return 0;
}

2,赶牛入圈

题目链接:https://www.acwing.com/problem/content/description/123/

这题考察了前缀和+二分+离散化

我们一步一步来考虑,题目要求找到最小的边长满足条件,那么我们就可以二分边长,来判定是否有满足条件的情况,二分的时间复杂度是O(logN)比一个一个遍历要快很多,所以这里用二分优化,接着,对于给定的边长,想求一个正方形内矩阵的值,我们可以用二维前缀和的方式快速求出来,因为这个题目的数据很大,有10000,如果我们遍历每个给定边长大小的矩阵的话肯定会超时,但是我们又注意到,给定的点数最多是五百,所以我们可以用离散化把一个比较大的矩阵映射到一个比较小的矩阵,保证他们的相对大小关系不变就可以了

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

typedef pair<int,int>pii;

const int N=1010;//开500的两倍大小,因为我们将横纵坐标都加到一个数组里去了

pii point[N];//存点
int sum[N][N],c,n;//sum表示离散化后的前缀和
vector<int>v;//离散化后的数组

int get(int x)//找到离散化后的数组下标,用于求离散化后的前缀和
{
    int l=0,r=v.size()-1;
    while(l<r)
    {
        int mid=l+r>>1;
        if(x<=v[mid])r=mid;
        else l=mid+1;
    }
    return r;
}
bool check(int len)//判断当前长度下是否可以满足条件
{
    //因为是离散化后的数组,所以相邻的两个数的距离不一定是1,所以要通过这种方式找到边长小于len的正方形
    for(int x1=0,x2=1;x2<=v.size();x2++)
    {
        while(v[x2]-v[x1]+1>len&&x1<=x2)x1++;//如果这两个点的横向距离大于len就将x1++
        for(int y1=0,y2=1;y2<=v.size();y2++)
        {
            while(y1<=y2&&v[y2]-v[y1]+1>len)y1++;//如果这两个点的纵向距离大于len,就将y1++
            //如果有满足条件的就返回true,说明边长可以继续缩小
            if(sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1]>=c)
                return true;
        }
    }
    return false;//没找到就返回false,说明要把边长增大
}
int main()
{
    cin>>c>>n;
    v.push_back(0);
    for(int i=0;i<n;i++)
    {
        int a,b;
        cin>>a>>b;
        v.push_back(a);//离散化其实是给出各数之间的大小关系,所以把横纵坐标加在一个区间没有影响
        v.push_back(b);
        point[i].first=a,point[i].second=b;//记录点
    }
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());//离散化
    
    for(int i=0;i<n;i++)//找到点对应的下标
    {
        int x=get(point[i].first),y=get(point[i].second);
        sum[x][y]++;//表示离散化后的这个点有草
    }
    for(int i=1;i<=v.size();i++)//求二维前缀和
        for(int j=1;j<=v.size();j++)
            sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
    int l=0,r=10000;//二分答案
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid))r=mid;
        else l=mid+1;
    }
    cout<<r;
    return 0;
}

3,防线

题目链接:https://www.acwing.com/problem/content/description/122/

这个题考察了二分,同样我们用二分转换为判定,题目要求我们求出奇数个数防具的位置,观察题目,可以发现这个题目里面最多出现一个奇数,所以我们就可以用二分可能的取值,去找是否有一段区间的个数为奇数,因为如果有一个位置为奇数的话,那么这段区间的和必定为奇数,我们不断二分,去找到这个区间,最后判断一下找到的这个位置是否是奇数

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

typedef long long ll;

const int N=200010;

struct stu
{
    int s,e,d;
}seg[N];
int n;

ll get(int x)//找到小于x的个数有几个
{
    ll res=0;//答案可能会爆int
    for(int i=0;i<n;i++)
        if(seg[i].s<=x)//如果这个等差数列的首项小于x才有个数
            res+=(min(seg[i].e,x)-seg[i].s)/seg[i].d+1;
    return res;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        int l=0,r=0;
        for(int i=0;i<n;i++)
        {
            scanf("%d%d%d",&seg[i].s,&seg[i].e,&seg[i].d);
            r=max(r,seg[i].e);//记录二分的右边界
        }
        while(l<r)
        {
            int mid=(ll)l+r>>1;//二分边界
            if(get(mid)&1)r=mid;//如果小于mid的个数有奇数个,说明mid前的那段区间必定有奇数
            else l=mid+1;
        }
        ll sum=get(r)-get(r-1);//判断找到的这个点的个数是否是奇数个,这里用到了前缀和的思想
        if(sum&1)
            printf("%d %lld\n",r,sum);
        else
            printf("There's no weakness.\n");
    }
    return 0;
}

4,任务

题目链接:https://www.acwing.com/problem/content/description/129/

贪心

对于每一个任务:

y的差异最多能使利润w浮动2*100=200元。x差1,则会使利润w浮动500元。

所以,y对利润的影响较小,x与其利润w的关系成对应关系(即x_{i}<x_{j},则w_{i}<w_{j},仅当x_{i}==x_{j},再按照y考虑即可。

策略可以变成以x从大到小的顺序考虑每一个任务,如果能匹配机器,则从匹配的机器中选择y最小的一个。

匹配机器:

1,先用x分别从大到小排序任务与机器

2,对于每一个任务,把时间充足的机器放入集合中。

3,若存在,从集合中找出级别最低的机器使用,并且从集合中删除那个机器。

这种处理顺序可以保证:在处理第一个任务时,集合中的机器时间都是充足的。且找机器的时间复杂度处于O(M+N)级别。

STL的multiset恰好支持从序列中查找大于等于某个数的最小值

代码如下:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <set>

#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 1e5 + 10;

int n,m;
pii ma[N],ta[N];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)scanf("%d%d",&ma[i].x,&ma[i].y);
    for(int i=0;i<m;i++)scanf("%d%d",&ta[i].x,&ta[i].y);
    sort(ma,ma+n,greater<pii>());//将机器以x为第一关键字,y为第二关键字,从大到小排序
    sort(ta,ta+m,greater<pii>());//将任务以x为第一关键字,y为第二关键字,从大到小排序

    multiset<int>s;
    ll cnt=0,res=0;
    for(int i=0,j=0;i<m;i++)
    {
        while(j<n&&ma[j].x>=ta[i].x)s.insert(ma[j++].y);//将所有大于当前任务的时间的机器都加入到集合中
        auto it=s.lower_bound(ta[i].y);//二分查找满足条件的最小等级的机器
        if(it!=s.end())
        {
            cnt++;
            res+=ta[i].x*500+ta[i].y*2;
            s.erase(it);//用过的机器要删去
        }
    }
    printf("%lld %lld",cnt,res);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值