二分答案+并查集+搜索+dp+最短路题组

- [P2814 家谱](https://www.luogu.com.cn/problem/P2814)

题解:这题很容易让人想到利用并查集来做,所以我这里是将名字借助map转成了对应的序号,从而合并、查询

#include<bits/stdc++.h>
using namespace std;
map<string,int>mp;
int fa[50100],nowFa;
int findd(int x)
{
    if(x==fa[x])return x;
    else
    {
        fa[x]=findd(fa[x]);
        return fa[x];
    }
}
//合并顺序要注意,只能是儿子合并到父亲
void unionn(int x,int y)
{
    //x是儿子,y是父亲
    int dx=findd(x),dy=findd(y);
    if(dx==dy)return;
    else
    {
        fa[dx]=dy;
        return;
    }
}
string findFatherName(int x)
{
    for(auto t:mp)
    {
        if(t.second==x)
        {
            return t.first;
        }
    }
    return NULL;
}
int main()
{
    char ch;
    cin>>ch;
    int index=1;
    while(ch!='$')
    {
        string name;
        cin>>name;
        if(ch=='#')
        {
            if(mp.count(name))
            {
                nowFa=findd(mp[name]);
            }
            else
            {
                mp[name]=index;
                fa[index]=index;
                nowFa=index;
                index++;
            }
        }
        else if(ch=='+')
        {
            if(mp.count(name))
            {
                unionn(mp[name],nowFa);
            }
            else
            {
                mp[name]=index;
                fa[index]=index;
                unionn(index,nowFa);
                index++;
            }

        }
        else if(ch=='?')
        {
            int father=findd(mp[name]);
            //cout<<"父亲序号:"<<father<<endl;
            cout<<name<<" "<<findFatherName(father)<<endl;
        }
        cin>>ch;
    }
    return 0;
}

我后面看到一篇题解:用stl的map把他和父亲直接连起来,不要转成序号再做要简单很多:

#include<bits/stdc++.h>
using namespace std;
map<string,string>p;
string findd(string x)
{
    if(x!=p[x])p[x]=findd(p[x]);
    return  p[x];
}
string s,nowFa;
int main()
{
    char ch;
    cin>>ch;
    while(ch!='$')
    {
        cin>>s;
        if(ch=='#')
        {
            nowFa=s;
            if(p[s]=="") p[s]=s;
        }
        else if(ch=='+')p[s]=nowFa;
        else cout<<s<<' '<<findd(s)<<endl;
        cin>>ch;
    }
    return 0;
}

- [P3958 奶酪](https://www.luogu.com.cn/problem/P3958)

题解:

1.搜索算法—深度优先

其实深度优先应该是最快的。

首先,我们找出所有可以从下表面进入的球,然后深度优先搜一遍。一旦遇到一个点最高处高度z+r≥h,就表示可以到上表面,退出。因为每个洞最多访问一次(只需要求是否能到达上表面,而不是最短步数),然后决定下一步的时候还需要O(n)的时间。所以总复杂度时O(n^2)。

实际上,往往不需要访问所有的洞就可以判断“Yes”,大多数情况下只有“No”的情况要访问全部。因此很少达到O(n^2)的最高复杂度。

2.搜索算法—广度优先

同上思想,但是初始时不是对于每个点跑一遍,而是把所有与下表面接触的洞直接加入广搜队列,然后搜索。复杂度仍然是O(n^2)。细节同上,但是往往跑的量比DFS更大。

3.并查集算法

我们可以为每一个球都建立一个结点,然后用O(n^2)的时间完成并查集处理,最后查询 与下表面接触的球体中 是否有和 与上表面接触的球体 在同一个集合中 的球体(有些拗口,理解就好)。然后输出“Yes”或“No”。这个算法应该常数略大,因为总是要跑完所有n^2次连接。

dfs做法如下:

#include<bits/stdc++.h>
using namespace std;
int t,n,flag;
long long h,r;
int book[1001];
struct Node
{
    double x,y,z;
} node[1001];
double getDistance(double x1,double y1,double z1,double x2,double y2,double z2)
{
    double distance=sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)+(z2-z1)*(z2-z1));
    return distance;
}

void dfs(int num)
{
    if(node[num].z+r>=h)
    {
        flag=1;
        return;
    }
    if(flag)return;
    book[num]=1;
    for(int i=1; i<=n; i++)
    {

        if(!book[i]&&getDistance(node[num].x,node[num].y,node[num].z,node[i].x,node[i].y,node[i].z)<=2*r)
        {
            dfs(i);
        }

    }
}
int main()
{
    cin>>t;
    while(t--)
    {
        cin>>n>>h>>r;
        memset(book,0,sizeof(book));
        for(int i=1; i<=n; i++)
        {
            cin>>node[i].x>>node[i].y>>node[i].z;
        }
        flag=0;
        for(int i=1; i<=n; i++)
        {
            if(node[i].z<=r&&!flag)
            {
                dfs(i);
            }
        }
        if(flag)
        {
            cout<<"Yes"<<endl;
        }
        else
        {
            cout<<"No"<<endl;
        }
    }
    return 0;
}

- [P2330 繁忙的都市](https://www.luogu.com.cn/problem/P2330)

题解:这题就是kruskal算法的板子,不多说了

#include<bits/stdc++.h>
using namespace std;
int n,m,flag,maxx,num;
int fa[310];
struct Road
{
    int u,v,c;
} road[100100];
bool compare(struct Road x,struct Road y)
{
    return x.c<y.c;
}
int findd(int x)
{
    if(x==fa[x])return x;
    else
    {
        fa[x]=findd(fa[x]);
        return fa[x];
    }
}
void unionn(int x,int y)
{
    int dx=findd(x),dy=findd(y);
    if(dx==dy)return;
    else
    {
        fa[dx]=dy;
        return;
    }
}
int main()
{
    cin>>n>>m;
    flag=n;
    for(int i=1; i<=n; i++)
    {
        fa[i]=i;
    }
    for(int i=1; i<=m; i++)
    {
        cin>>road[i].u>>road[i].v>>road[i].c;
    }
    sort(road+1,road+m+1,compare);
    for(int i=1; i<=m; i++)
    {
        if(flag!=1)
        {
            if(findd(road[i].u)!=findd(road[i].v))
            {
                unionn(road[i].u,road[i].v);
                flag--;
                num++;
                maxx=max(maxx,road[i].c);
            }

        }
        else break;
    }
    cout<<num<<" "<<maxx;
    return 0;
}

- [P1340 兽径管理](https://www.luogu.com.cn/problem/P1340)

题目大意: 加m次边,每一次加边让你求最小生成树

如果每加一次边就跑一次 Kruskal ,时间复杂度会非常高,所以我们要考虑节约时间的方法。

显然从第一周跑到最后一周太耗时间,于是我们就会想到从最后一周开始,逆序跑kruskal

这样的好处在于,一旦发现某一周不能构成最小生成树,那么那周之前也不可能构成最小生成树,于是我们可以少跑很多次kruskal

因为只能用那一周及之前的兽径建树,而在快排的时候边会被打乱,所以我们在结构体存边时要加一个参数,记录该边是第几周的兽径

剩下的就是kruskal的基本操作了

#include<bits/stdc++.h>
using namespace std;
int n,w;
struct Edge
{
    int start;
    int endd;
    int value;
    int time;
} edge[6001];
int fa[301],ans[6001];
bool compare(struct Edge x,struct Edge y)
{
    return x.value<y.value;
}
int findd(int x)
{
    if(fa[x]==x)return x;
    else
    {
        fa[x]=findd(fa[x]);
        return fa[x];
    }
}

int kruskal(int time)//每次kruskal时对于并查集用到的fa[]数组都要先初始化一次
{
    int sum=0,num=0;
    for(int i=1; i<=n; i++)fa[i]=i;
    for(int i=1; i<=w; i++)
    {
        if(num==n-1)break;
        if(edge[i].time<=time)
        {
            int x=findd(edge[i].start),y=findd(edge[i].endd);
            if(x!=y)
            {
                fa[x]=y;
                sum+=edge[i].value;
                num++;
            }
        }
    }
    if(num==n-1)return sum;
    else return -1;
}
int main()
{
    cin>>n>>w;
    for(int i=1; i<=w; i++)
    {
        cin>>edge[i].start>>edge[i].endd>>edge[i].value;
        edge[i].time=i;
    }
    sort(edge+1,edge+1+w,compare);
    //逆序克鲁斯卡尔
    for(int i=w; i>=1; i--)
    {
        ans[i]=kruskal(i);
        if(ans[i]==-1)
        {
            for(int j=1; j<i; j++)ans[j]=-1;
            break;
        }

    }
    for(int i=1; i<=w; i++)cout<<ans[i]<<endl;
    return 0;
}

- [Mzc和体委的争夺战](https://www.luogu.com.cn/problem/P2299)

题解:堆优化的迪杰斯特拉算法的板子,需要注意的是因为是无向边,所以记录边的数组需要开成两倍

#include<bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
int n,m;
struct Road
{
    int to,value,next;
} road[410000];//双向边,数组应该开两倍
int book[2510],head[2510],cnt;
int dis[2510];
struct Node
{
    int index;
    int distance;
    bool operator < (const Node &x)const
    {
        return distance>x.distance;
    }
};
priority_queue<Node>q;
void add(int a,int b,int c)
{
    cnt++;
    road[cnt].to=b;
    road[cnt].value=c;
    road[cnt].next=head[a];
    head[a]=cnt;
}
void dij()
{
    q.push({1,0});
    while(!q.empty())
    {
        Node t=q.top();
        q.pop();
        int index=t.index;
        if(book[index])continue;
        book[index]=1;
        for(int i=head[index]; i!=-1; i=road[i].next)
        {
            int a=index,b=road[i].to,value=road[i].value;
            if(dis[b]>dis[a]+value)
            {
                dis[b]=dis[a]+value;
                q.push({b,dis[b]});
            }
        }
    }

}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        head[i]=-1;
        dis[i]=inf;
    }
    dis[1]=0;
    for(int i=1; i<=m; i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
        add(b,a,c);
    }
    dij();
    cout<<dis[n];
    return 0;
}

- [数列分段 Section II](https://www.luogu.com.cn/problem/P1182)

题解:二分答案的例题,关于二分答案不懂的可以看看 二分-答案(板子)_小白_学编程的博客

需要注意的是这里答案最小可能是数列中的最大值(即left),最大可能是所有数的和(即right)

#include<bits/stdc++.h>
using namespace std;
long long rightt=1e9+1,leftt;
long long vis[100010];
int n,m,ans;
bool checkMid(long long x){
    long long sum=0;
    int num=0;
    for(int i=1;i<=n-1;i++){
        sum+=vis[i];
         if(sum+vis[i+1]>x){
            num++;
            sum=0;
        }
    }
    if(num+1<=m)return true;//这里注意是num+1
    else return false;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>vis[i];
        leftt=max(leftt,vis[i]);
    }
    while(leftt<=rightt){
        long long mid=(leftt+rightt)/2;
        if(checkMid(mid)){
            ans=mid;
            rightt=mid-1;
        }else{
            leftt=mid+1;
        }
    }
    cout<<ans;
    return 0;
}

- [P1873 EKO / 砍树](https://www.luogu.com.cn/problem/P1873)

题解:二分答案的例题

#include<bits/stdc++.h>
using namespace std;
vector<long long>vc;
long long l,r;
long long n,m,maxx,ans;
bool checkMid(long long x)
{
    long long nums=0;
    for(int i=0; i<n; i++)
    {
         if(vc[i]>x){
            nums+=vc[i]-x;
         }
    }
    if(nums>=m)return true;
    else return false;
}
int main()
{
    cin>>n>>m;
    for(int i=0; i<n; i++)
    {
        long long number;
        cin>>number;
        vc.push_back(number);
        maxx=max(maxx,number);
    }
    r=maxx;
    while(l<=r)
    {
        long long mid=(r+l)/2;
        if(checkMid(mid)){
            ans=mid;
            l=mid+1;
        }
        else r=mid-1;
    }
    cout<<ans;
    return 0;
}

- [P1577 切绳子](https://www.luogu.com.cn/problem/P1577)

题解:二分答案的例题,这题可以先把小数乘以100化为整数,在整数域上二分,输出时再除以100即可。我这里是直接在实数域上二分,但是这题有点古怪的是如果只保留两位小数过不了...

#include<bits/stdc++.h>
using namespace std;
double vis[10010],l,r,mid;
int n,k;
bool checkMid(double x){
    int num=0;
    for(int i=1;i<=n;i++){
        num+=vis[i]/x;
    }
    if(num>=k)return true;
    else return false;
}
int main()
{

    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>vis[i];
        r+=vis[i];
    }
    while(r-l>1e-4){
        mid=(r+l)/2;
        if(checkMid(mid)){
            l=mid;
        }else{
            r=mid;
        }
    }
    printf("%.6lf",l);
    return 0;
}

- [P1387 最大正方形](https://www.luogu.com.cn/problem/P1387)

题解:用动态规划解,用dp(i, j) 表示以 (i, j)为右下角, 且只包含 1 的正方形的边长最大值。

  • 对于每个位置 (i,j)如果该位置的值是 0,则 dp(i,j)=0,因为当前位置不可能在由 1 组成的正方形中;
  • 如果该位置的值是 1,则 dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 dp 值决定。具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 1,状态转移方程如下:

    dp(i,j)=min(dp(i−1,j),dp(i−1,j−1),dp(i,j−1))+1

#include<bits/stdc++.h>
using namespace std;
int n,m;
int vis[110][110];
int dp[110][110],ans;
int main()
{
    cin>>n>>m;
    for(int i=1; i<=n; i++)
    {
        for(int j=1; j<=m; j++)
        {
            cin>>vis[i][j];
        }
    }
    for(int i=1; i<=n; i++)
    {
        for(int j=1; j<=m; j++)
        {
            if(vis[i][j]==0)dp[i][j]=0;//其实可以省略,因为默认是0
            else
            {
                if(i==1||j==1)dp[i][j]=1;
                else
                {
                    dp[i][j]=min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
                }
            }
            ans=max(dp[i][j],ans);
        }
    }
    cout<<ans;
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值