AcWing暑假每日一题

AcWing 3775. 数组补全(南昌理工学院)


魔法阵.

题目介绍

给定一个 1∼n 的排列 f1,f2,…,fn。

已知,对于 1≤i≤n,fi≠i 始终成立。

现在,因为一些原因,数组中的部分元素丢失了。

请你将数组丢失的部分补全,要求数组在补全后仍然是一个 1∼n 的排列,并且对于 1≤i≤n, fi≠i 均成立

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据第一行包含一个整数 n。

第二行包含 n 个整数 f1,f2,…,fn。如果 fi=0,则表示 fi 已经丢失,需要补全。

输出格式
每组数据一行,输出补全后的 f 数组,整数之间空格隔开。

如果方案不唯一,则输出任意合理方案即可。

数据范围
1 ≤ T ≤ 100,
2 ≤ n ≤ 2×105,
0≤ fi ≤n,至少两个 fi 为 0。
同一测试点内所有 n 的和不超过 2×10^5。
数据保证有解

输入样例:
3
5
5 0 0 2 4
7
7 0 0 1 4 0 6
7
7 4 0 3 0 5 1
输出样例:
5 3 1 2 4
7 3 2 1 4 5 6
7 4 2 3 6 5 1

解题思路

题目大意:
给我们一个1-n的全排列,用f(i)来表示,但是有一些元素缺失了,即f(i)=0,我们需要填上这个空缺,并且满足f(i)!=i
方法一:问题是要我们满足缺失位置的f(i)!=i,那我就令缺失位置f(i)=i,然后将其错位,这样就能保住f(i)!=i了,问题来了,当前位置的数,出现过怎么办?
例如样例一
缺失数组 5 0 0 2 4
猜测数组 5 2 3 2 4

我们将所有给出的f(i)全部记录下来,并将其用一个数组标记下来,如果这个数是零的话,我们将其下标存储在一个新的数组里,遍历存储下表数组,如果这个数没有出现过,我们将其存储在一个答案数组中,最后从1-n遍历一遍如果这个数没有出现过,且当答案数组没出现过,就将这个数存储在答案数组中,最后从一到n遍历,如果这个数不是零,我们就输出原数组,否则就输出答案数组(错位输出)
上代码

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;
const int N=200010;
int f[N];//原数组
int ans[N],a[N];//答案数组,中间数组
bool st[N];//标记数组
int main()
{
    int T;
    scanf("%d",&T);
    while(T --)
    {
        memset(ans, 0, sizeof ans);//多组输入,全部置零
        memset(st, 0, sizeof st);
        int n;
        int cnt=0;
        scanf("%d",&n);
        for(int i=1;i<=n;i++) //输入原数组,标记出现过的数,如果这个数是零,表示缺失数,记录到中间数组中其值为下表,为了让f(i)=i
        {
            scanf("%d",&f[i]);
            st[f[i]]=true;
            if(f[i]==0) a[cnt++]=i;
        }
        for(int i=0;i<cnt;i++)//遍历中间数组,如果这个数没有出现过,放在答案数组中
            if(!st[a[i]]) ans[i]=a[i],st[a[i]]=1;
        
        int j = 0;

        for(int i = 1 ;i <=n ;i ++)//从1-n遍历,如果这个数没有出现过,说明它应该在答案数组中,将它存到答案数组中
        {
            if(!st[i])
            {
                while(ans[j]) j++;
                ans[j] = i;
            }
        }
        j = 0;
        for(int i = 1;i <= n ;i ++)//遍历输出,如果原数组是零就将答案数组相错输出,
        {
            if(!f[i])
            {
                cout << ans[(j+1) % cnt]<<" ";
                j ++;
            }
            else cout<< f[i] << " ";
        }
        puts("");
    }
	return 0;
}

方法二
本题是将f(i)!=i,那如果相等的话就是一个从1开始的公差为一的单调递增的序列,那我们可以直接将这个序列反转,从n到1,这样就能满足f(i)!=i了,但是如果是下面这个例子该怎么办呢?
7(长度为7)
0 0 0 0 0 0 0
我们填上 7 6 5 4 3 2 1
我们发现f(4)=4,那么我将一个数与这个数交换就ok了,那么就为
4 6 5 7 3 2 1
多个例子发现,最多只会出现一次f(i) =i,如何解决这种情况呢?
我们将缺失的数的下标记录下来,,其大小为m,从大到小排序,将第一个插入的值或者最后一个插入的值进行交换,但由于 f(i) 可能是 f(1) 和 f(m) 其中的一个(也即在 1 或 m 位置出现了冲突),所以需要记录两个位置,选择与其中一个没有冲突的位置互换。
上代码

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200010;
int a[N],b[N];
bool st[N];

bool cmp(int x,int y)
{
    return x>y;
}


int main()
{
    int t;
    scanf("%d", &t);
    while (t -- )
    {
        memset(st,0,sizeof st);
        int n;
        int cnt=0;
        scanf("%d", &n);
        for (int i = 1; i <= n; i ++ )//记录缺失数组,将所有非零的数打上标记
        {
            scanf("%d", &a[i]);
            if(a[i]) st[a[i]]=true;
        }
        for (int i = 1; i <= n; i ++ )//将所有为零的数的下标存到b数组中
        {
            if(!st[i]) b[cnt++]=i;
        }
        sort(b,b+cnt,cmp);//从大到小排列
        int j = 0, l, r;
        for(int i = 1; i <= n; i++) //找到第一个插入的值和最后一个值
        {
            if(a[i] == 0) 
            {
                a[i] = b[j],j++;
                if(j == 1) l = i;
                else if(j == cnt) r = i;
            }
        }
        for (int i = 1; i <= n; i ++ )//交换发生冲突的数
        {
            if(a[i]==i)
            {
                if(i!=l) swap(a[i],a[l]);
                else swap(a[i],a[r]);
                break;
            }
        }
        for (int i = 1; i <= n; i ++ ) printf("%d ",a[i]);
        puts("");
    }
    return 0;
}

方法三
将这个看成是一个图,如果这个f(i)!=0,我们就将i->f(i),连一条边
样例可以看出是1——>5——>4——>2,我们将其构成一个链,我们要保证f(i)!=i,j即没有一条连向自己边,就是没有自环,然后我们将没有出现过的点直接加入这个链中,构成一个环,这样就满足条件了,还有一种情况就是,如果已经构成一个环的话,怎么办呢?我就所有没有出现过的数直接成环
思想很简单,代码超级难

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200010;

int n;
int p[N], q[N];//出边和入边
bool st[N];//标记数组

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n);
        memset(q, 0, sizeof q);
        memset(st, 0, sizeof st);
        for (int i = 1; i <= n; i ++ )//i->p[i],p[i]的反向边就是i
        {
            scanf("%d", &p[i]);
            q[p[i]] = i;
        }

        bool flag = false;
        for (int i = 1; i <= n; i ++ )
        {
            if (st[i] || !p[i]) continue;//如果这个点已经处理了,或者这个点是0,我们就跳过
            st[i] = true;
            int x = i, y = i;
            while (p[x] && !st[p[x]])//找到头节点
            {
                x = p[x];
                st[x] = true;
            }
            while (q[y] && !st[q[y]])//找到尾节点
            {
                y = q[y];
                st[y] = true;
            }

            if (p[x] == y) continue;//如果联通就直接continue
            if (!flag)//处理孤立点
            {
                flag = true;

                for (int j = 1; j <= n; j ++ )
                    if (!p[j] && !q[j])
                    {
                        st[j] = true;
                        p[x] = j;
                        x = j;
                    }
            }
            p[x] = y;
        }

        if (!flag)//处理没有加入环中的点
        {
            int x = 0, y = 0;
            for (int i = 1; i <= n; i ++ )
                if (!p[i])
                {
                    if (!x && !y) x = y = i;
                    else
                    {
                        p[x] = i;
                        x = i;
                    }
                }
            p[x] = y;
        }

        for (int i = 1; i <= n; i ++ )
            printf("%d ", p[i]);
        puts("");
    }

    return 0;
}

ps:如果不理解第三种方法,详细请看y总的讲解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值