2019.6.10 校内测试 分析+题解

2017 NOIP 模拟赛

T1  FBI树  传送门

T2  医院设置  传送门

T3  加分二叉树  传送门

我个人感觉T3挺难的(肯定是因为太弱了,前序遍历和中序遍历都不知道),T1和T2还好,至少在洛谷上AC了,不知道评测机咋了我T2爆零(哭 ;

哎,毕竟是一次小测试是吧,还好不是真正的NOIP,提前找到自己的不足,在此写博客记录此次的收获!

P1087 FBI树

这一道题挺简单的,手动模拟一下就好了,我们对样例进行模拟:

我们发现:输入的字符串会在第n层分解成为2^n个单独的字符;

我们可以反过来看:根结点所得到的字符串就是它的两个儿子结点拼起来的!

为什么这么看呢?因为我们知道根节点是F型的,将它拆分需要扫一遍才知道它的左右儿子是什么型的;但是我们已经知道叶子结点一定是B型或I型了,这样我们一层一层推上去就好了;

很显然有下面五种运算:

1.B+B=B;

2.I+I=I;

3.B+I=F;

4.B+F=F;

5.I+F=F;

贴一下后序遍历:

后序遍历(LRD)是二叉树遍历的一种,也叫做后根遍历、后序周游,可记做左右根。后序遍历有递归算法和非递归算法两种。在二叉树中,先左后右再根,即首先遍历左子树,然后遍历右子树,最后访问根结点。

然后就是我的代码啦:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
int read()                        //读入优化 
{
    char ch=getchar();
    int a=0,x=1;
    while(ch<'0'||ch>'9')
    {
        if(ch=='-') x=-x;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        a=(a<<3)+(a<<1)+(ch-'0');
        ch=getchar();
    }
    return a*x;
}
int n;
char ans[21000],m;                //ans数组存储每个结点是什么型的 
bool vis[21000];                  //后序遍历要用到 
int l[12000],r[12000];            //左右儿子的编号 
void dfs(int now)                 //后序遍历输出 
{
    if(l[now]==0||vis[now*2]==1&&vis[now*2+1]==1)  //如果该结点没有儿子(叶结点)或儿子都已经遍历过了,就可以输出它了 
    {
        printf("%c",ans[now]);
        vis[now]=1;
        return ;
    }
    if(l[now]!=0&&vis[now*2]==0) dfs(now*2);       //如果有左儿子却未遍历过,先找左儿子 
    if(r[now]!=0&&vis[now*2+1]==0) dfs(now*2+1);   //如果有右儿子却未遍历过,再找右儿子 
    printf("%c",ans[now]);                         //最后再输出自己 
    return ;
}
int main()
{
    //freopen("fbi.in","r",stdin);
    //freopen("fbi.out","w",stdout);
    scanf("%d",&n);
    int len=pow(2,n);             //求出这个字符串的长度 
    for(int i=len;i<2*len;i++)    //这里我们将每个单个字符摘了下来,从下标为2^n开始存 
    {
        cin>>m;
        if(m=='1') ans[i]='I';    //判断类型 
        if(m=='0') ans[i]='B'; 
    }
    for(int k=n;k>=0;k--)         //枚举层数,倒着往上推 
    {
        for(int i=pow(2,k);i<pow(2,k+1);i+=2)  //枚举每一层的结点
        
        {
            if(ans[i]=='I'&&ans[i+1]=='I') ans[i/2]='I';   //和为I型的情况 
            if(ans[i]=='B'&&ans[i+1]=='B') ans[i/2]='B';   //和为B型的情况 
            if(ans[i]=='B'&&ans[i+1]=='I'||ans[i]=='I'&&ans[i+1]=='B'||ans[i]=='F'||ans[i+1]=='F') ans[i/2]='F';  //和为F型的情况 
        }
    }
    //到这里我们就将这棵树建立起来了,接下来就是后序遍历了 
    for(int i=1;i<len*2;i++)     //初始化 
    {
        l[i]=0;         
        r[i]=0;
        vis[i]=0;
    }
    for(int i=1;i<len;i++)       //记录0~n-1层每个结点的儿子 
    {
        l[i]=i*2;
        r[i]=i*2+1;
    }
    dfs(1);                      //从根结点开始找 
    return 0;
} 

这是wz大佬(并列rank1的神仙)的代码%%%:

#include <iostream>
#include <string>
using namespace std;
int n;
string s;
char dfs(int l, int r)
{
    if (l == r)
    {
        if (s[l] == '0')
        {
            cout << 'B';
            return 'B';
        }
        else if (s[l] == '1')
        {
            cout << 'I';
            return 'I';
        }
    }
    int mid = (l + r) / 2;
    char le = dfs(l, mid);
    char ri = dfs(mid + 1, r);
    if (le == 'B' && ri == 'B')
    {
        cout << 'B';
        return 'B';
    }
    if (le == 'I' && ri == 'I')
    {
        cout << 'I';
        return 'I';
    }
    cout << 'F';
    return 'F';
}
int main()
{
    //freopen("fbi.in", "r", stdin);
    //freopen("fbi.out", "w", stdout);
    cin >> n >> s;
    dfs(0, (1 << n) - 1);
    cout << endl;
}

P1364 医院设置

这道题貌似用Floyed,但是我怎么跟别人这么不一样,我是暴力枚举每个点作为医院,然后再求出最小值的(难道这就是我这个题爆零的原因?);

但是洛谷上AC了,说明算法还是没问题的qwq。

说下我的思路:

由于这个题每个点都可能作为根结点,所以我在存边的时候不仅要记录当前结点的儿子有谁,还要记录那个儿子的父亲是当前结点,目的就是让这两个结点联通(就是无论访问哪个点都能找到另一个点:儿子找父亲,父亲找儿子);

然后我们枚举每一个点作为根结点,模拟一遍:从当前点出发,同时用一个k来表示走了多少条边,我们可以按照“父亲---左儿子---右儿子”的顺序依次走,每走一层k++,用vis数组来存当前结点是否走过,ans+=k*当前结点的人数,最后再讲ans取个最小值就是答案了qwq:

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
int read()
{
    char ch=getchar();
    int a=0,x=1;
    while(ch<'0'||ch>'9')
    {
        if(ch=='-') x=-x;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        a=(a<<3)+(a<<1)+(ch-'0');
        ch=getchar();
    }
    return a*x;
}
struct city
{
    int poeple,lc,rc,father;
}a[101];
int n,vis[101],ans=0,minx;              
void search(int x,int k)                 //k是已经走过的边数 
{
    if(a[x].father!=0&&vis[a[x].father]==0) //有父亲且没走过,那就先走父亲 
    {
        ans+=a[a[x].father].poeple*k;
        vis[a[x].father]=1;
        search(a[x].father,k+1);         //从父亲接着往下走 
    }
    if(a[x].lc!=0&&vis[a[x].lc]==0)      //走左儿子   
    {
        ans+=a[a[x].lc].poeple*k;
        vis[a[x].lc]=1;
        search(a[x].lc,k+1);        
    }
    if(a[x].rc!=0&&vis[a[x].rc]==0)      //走右儿子 
    {
        ans+=a[a[x].rc].poeple*k;
        vis[a[x].rc]=1;
        search(a[x].rc,k+1);
    }
    return ;                             //返回 
}
int main()
{
    //freopen("hospital.in","r",stdin);
    //freopen("hospital.out","w",stdout);
    minx=1e8;                            //将minx初始化成一个很大的值 
    n=read();
    for(int i=1;i<=n;i++) a[i].father=0; //初始化每个结点都没有父亲 
    for(int i=1;i<=n;i++)
    {
        a[i].poeple=read();
        a[i].lc=read();
        a[i].rc=read();
        if(a[i].lc) a[a[i].lc].father=i; //记录儿子的父亲是当前结点 
        if(a[i].rc) a[a[i].rc].father=i;
    }
    for(int i=1;i<=n;i++)                //枚举每个结点作为根结点 
    {
        memset(vis,0,sizeof(vis));       //注意清空 
        ans=0;                          
        vis[i]=1;
        search(i,1);                     //从根结点开始走 
        minx=min(minx,ans);
    }
    cout<<minx;
    return 0;
}

然后是Floyed算法的代码(好像挺好理解的,还挺简单qwq):

#include<iostream>
#include<cstring>
using namespace std;
const int inf=100000007;
int p[101],dis[101][101],sum;
int n,lch,rch;
int main()
{
    freopen("hospital.in","r",stdin);
    freopen("hospital.out","w",stdout); 
    cin>>n;
    memset(dis,inf,sizeof(dis));
    for(int i=1;i<=n;i++)
    {
        dis[i][i]=0;                                   //自己到自己的距离为0 
        cin>>p[i];
        cin>>lch>>rch;
        if(lch>=0) dis[i][lch]=1;dis[lch][i]=1;        //建双向图 
        if(rch>=0) dis[i][rch]=1;dis[rch][i]=1;
    }
    for(int k=1;k<=n;k++)                              //Floyed算法求出任意两点间的线段数 
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(dis[i][j]>dis[i][k]+dis[k][j]) dis[i][j]=dis[i][k]+dis[k][j];
            }
        }
    }
    int minn=inf;
    for(int i=1;i<=n;i++)                              //枚举每个点作为根结点 
    {
        sum=0;
        for(int j=1;j<=n;j++)                             
        {
            sum+=p[j]*dis[i][j];
        }
        if(minn>sum) minn=sum;
    }
    cout<<minn<<endl;
    return 0;
}

这是wz大佬DP做法%%%tql :

#include <iostream>
#include <queue>
#include <cstring>
#include <limits.h>
using namespace std;
int n;
int lch[101], rch[101], fa[101], sum[101];
int dis[101][101];
bool vis[101];
int root;
int main()
{
    freopen("hospital.in", "r", stdin);
    freopen("hospital.out", "w", stdout);
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> sum[i] >> lch[i] >> rch[i];
        fa[lch[i]] = fa[rch[i]] = i;
    }
    for (int i = 1; i <= n; i++)
    {
        root = i;
    }
    for (int i = 1; i <= n; i++)
    {
        queue<int> q;
        q.push(i);
        memset(vis, 0, sizeof(vis));
        vis[i] = 1;
        dis[i][i] = 0;
        while (!q.empty())
        {
            int node = q.front();
            q.pop();
            if (lch[node] && !vis[lch[node]])
            {
                q.push(lch[node]);
                vis[lch[node]] = 1;
                dis[i][lch[node]] = dis[i][node] + 1;
            }
            if (rch[node] && !vis[rch[node]])
            {
                q.push(rch[node]);
                vis[rch[node]] = 1;
                dis[i][rch[node]] = dis[i][node] + 1;
            }
            if (fa[node] && !vis[fa[node]])
            {
                q.push(fa[node]);
                vis[fa[node]] = 1;
                dis[i][fa[node]] = dis[i][node] + 1;
            }
        }
    }
    int ans, ansv = INT_MAX;
    for (int i = 1; i <= n; i++)
    {
        int nowans = 0;
        for (int j = 1; j <= n; j++)
        {
            nowans += dis[i][j] * sum[j];
        }
        if (nowans < ansv)
        {
            ansv = nowans;
            ans = i;
        }
    }
    cout << ansv << endl;
}

 

P1040 加分二叉树

 

注:

前序遍历(DLR),是二叉树遍历的一种,也叫做先根遍历、先序遍历、前序周游,可记做根左右。前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。

中序遍历(LDR),是二叉树遍历的一种,也叫做中根遍历、中序周游。在二叉树中,中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树。

 

题目要求说求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。也就是说这棵树的根结点左边都是左子树,右边都是右子树。

若3是根结点,那么它的左边的结点都在它的左子树里,右边的结点都在它的右子树里(因为中序遍历是按照“左---根---右”输出的);

所以我们只要记录好一个区间的根,就可以知道它的左子树和右子树了,这也方便了我们以后的前序遍历,我们就用root[i][j]来表示区间[i,j]这些结点中的根结点是谁;

然后我们可以用区间DP来解决这个题:

用数组f[i][j]来表示在区间[i,j]内的最大加分,这样考虑的话最后的答案一定是f[1][n]了。然后按照区间DP的套路,我们要枚举区间长度和区间位置:

    for(int i=1;i<n;i++)      //枚举区间长度 for(int j=1;i+j<=n;j++)//枚举区间位置,这里j是区间左端点的位置

有了区间长度i和左端点位置j,那么右端点位置显然是t=i+j ;然后我们求这个区间的最大加分,我们先以这个区间的左端点为根的最大加分算出来,然后依次枚举j+1~t分别作为根所求出的最大加分,看看是否大于以左端点为根的最大加分,大于就将它替换,同时将root[j][t]赋值为最大加分更大的所对应的那个根:

    for(int i=1;i<n;i++)      //枚举区间长度 
    {
       for(int j=1;i+j<=n;j++)//枚举区间位置,这里j是区间左端点的位置 
       {
              int t=i+j;      //区间右端点 
              f[j][t]=f[j][j]+f[j+1][t];   //先以左端点为根 
              root[j][t]=j;                //更新区间[j,t]的根为左端点j 
              for(int k=j+1;k<=t;k++)      //枚举区间内其他的点作为根结点 
              {
                    if(f[j][t]<f[j][k-1]*f[k+1][t]+f[k][k])    //如果发现了更大的加分就更新这个最大加分 
                    {
                          f[j][t]=f[j][k-1]*f[k+1][t]+f[k][k];
                          root[j][t]=k;                        //同时将区间[j,t]的根结点更新为k 
              }
           }
       } 
    } 

然后解决最后一道难关:前序遍历!!

因为前序遍历是按照“根---左---右”的顺序遍历的

所以我们先输出整棵树[1,n]的根结点root[1,n],然后分别再输出左子树和右子树,而左子树和右子树也要前序遍历按照“根---左---右”的顺序遍历,那就先输出左子树的根,再将左子树细分成小左子树和小右子树(暂时先这么叫吧),……循环往复,一直到了叶结点没有子树了就返回。所以前序遍历我们就用递归做:

void qx(int l,int r)                    //前序遍历:根---左---右 
{
    if(l>r) return ;                    //如果不合法直接返回 
    printf("%d ",root[l][r]);           //先输出根结点 
    if(l==r) return ;                   //如果就一个点那就返回 
    qx(l,root[l][r]-1);                 //然后输出左子树 
    qx(root[l][r]+1,r);                 //最后输出右子树 
}

完整代码如下:

#include<iostream>
#include<cstdio>
using namespace std;
int n,f[31][31],root[31][31],a[31];     //dp[i][j]存储区间[i,j]的最大加分,root[i][j]存储区间[i,j]的根结点编号 
void qx(int l,int r)                    //前序遍历:根---左---右 
{
    if(l>r) return ;                    //如果不合法直接返回 
    printf("%d ",root[l][r]);           //先输出根结点 
    if(l==r) return ;                   //如果就一个点那就返回 
    qx(l,root[l][r]-1);                 //然后输出左子树 
    qx(root[l][r]+1,r);                 //最后输出右子树 
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) 
    {
       cin>>a[i];    
    }
    for(int i=1;i<=n;i++)
    {
       f[i][i]=a[i];          //一开始区间[i,i]也就是点i,值就是a[i] 
       root[i][i]=i;          //区间[i,i]的根肯定是i,因为就它一个元素 
    }
    for(int i=1;i<n;i++)      //枚举区间长度 
    {
       for(int j=1;i+j<=n;j++)//枚举区间位置,这里j是区间左端点的位置 
       {
              int t=i+j;      //区间右端点 
              f[j][t]=f[j][j]+f[j+1][t];   //先以左端点为根 
              root[j][t]=j;                //更新区间[j,t]的根为左端点j 
              for(int k=j+1;k<=t;k++)      //枚举区间内其他的点作为根结点 
              {
                    if(f[j][t]<f[j][k-1]*f[k+1][t]+f[k][k])    //如果发现了更大的加分就更新这个最大加分 
                    {
                          f[j][t]=f[j][k-1]*f[k+1][t]+f[k][k];
                          root[j][t]=k;                        //同时将区间[j,t]的根结点更新为k 
              }
           }
       } 
    } 
    cout<<f[1][n]<<endl;      //此时的f[1][n]就是区间[1,n]也就是整棵树的最大加分 
    qx(1,n);                  //前序遍历 
    return 0;
}

 

 

posted @ 2019-06-10 15:57  暗い之殇  阅读(225)  评论(0编辑  收藏  举报