方法精讲-“链表法”+例题精讲《移动盒子》——伟大的旭哥的博客

1.链表

        链表不要求元素的存储位置连续,因此为了表示数据元素之间的关系,对于每个元素需要增加指针域来记录其直接后继和直接前驱的位置

        只记录后继或前驱位置的链表称为单向链表,即记录前驱又记录后继的链表称为双向链表

        链表中一般会有一个头指针(head)指向首元素或者尾指针(tail)指向尾元素。

2.链表的实现

         实际使用中,一般通过结构体数组来模拟链表,指针域记录后继/前驱在数组中的下标。

        链表的查找复杂度为O(N) ,插入和删除的时间复杂度为O(1) (只需要修改指针域3)

3.链表的作用

        链表一般应用在只知道数据总量,不确定各部分数据量的题目中,用来节省存储空间

                (1)示例:给定N( 2 ≤ N ≤ 10^5 个人,编号1…N,其中有M 1 ≤ M≤10^6 对人之间有亲戚关系,给定这M对人,输出每个人的所有亲戚。

                输入: 4 5 1 2 1 3 2 3 4 1 2 4

                输出:2 3 4 1 3 4 1 2 1 2

        思路:为每个人分配1个独立的线性表,用来存储其所有亲戚,输出时遍历线性表。

        顺序表实现:每个人至多可能有N-1个亲戚,每个数组的大小至少为N-1,共N(N-1)。

        使用链表实现,由于只有M对人之间有亲戚关系,每对关系需要记录两次,最多2*M条记录

        每个链表需要一个指向首条元素的head指针,N个链表可以定义数组存储

|-------------------------------------------------------------------------------------------------------------------------------|

关键来了!!!!!

        当插入、删除、移动位置操作比较多时,使用数组必定会超时,此时就可以使用链表来进行时间优化

        使用双向链接记录每个数据的左指针、右指针以及数据值,其中指针指的是数组下标

        (1)插入操作

        0 a1 2    1 a2 3   2 a3 0

         例如,在a1和a2之间插入a4,进行如下操作

        ① 将a4.left赋值为a2的left a4.left=a2.left

        ② 将a4的rigth赋值为2 a4.right=2;

        ③将a1的rigth赋值为4 a1.right=4;

        ④ 将a2的left赋值为4 a2.left=4 1 a4 2

        0 a1 4    4 a2 3    2 a3 0   1  a4  2

        (2)删除操作

        0 a1 2   1 a2 3   2 a3 0

        例如,将a2删除,进行如下操作:

        ① 将a2的right对应下标的值的left更改为a2的left a[a2.right].left=a2.left

        ② 将a1right赋值为a2的right a1.right=a2.right

        0 a1 3    1 a3 0

        (3)移动操作

        0 a1 2   1 a2 3   2 a3 4   3 a4 0

        例如,将a3移动到a2的左边,进行如下操作:

        ① 将a3的left赋值为a2的left a3.left=a2.left

        ② 将a2的right赋值为a3的right a2.right=a3.right

        ③ 将a3的right下标对应的元素的left赋值为2 a[a3.right].left=2

        ④ 将a3的right赋值为2 a3.right=2

        ⑤ 将a2left对应下标的元素的right赋值为3,a[a2.left].right=3;

        ⑥ 将a3left赋值为a2的left,a3.left=a2.left;

        ⑦ 将a2left赋值为3,a2.left=3;

        0 a1 3   3 a2 4   1 a3 2   2 a4 0

2.例题精讲:《移动盒子》

题目描述

桌子上有n个盒子排成一条直线,从左到右编号是1到n,你的问题是模拟下面4中操作:
1 X Y:将编号为x的盒子移动到编号为Y的盒子的左边(如果X已经在Y的左边了,则忽略)
2 X Y:   将编号为X的盒子移动到编号为Y的盒子的右边(如果X已经在Y的又变了,则忽略)
3 X Y:将编号为X的盒子与编号为Y的盒子交换位置
4 : 将桌子上的盒子进行反转
给定的所有操作命令都是合法的,不会出现X等于Y的情况。
例如:n=6,一开始盒子是按照如下顺序摆放的:1 2 3 4 5 6,执行操作1 1 4后,顺序为2 3 1 4 5 6,再执行操作2 3 5,执行后顺序为2 1 4 5 3 6,再执行操作3 1 6,执行后顺序为2 6 4 5 3 1,然后再执行操作4,执行后顺序为1 3 5 4 6 2

输入格式

输入为多组数据,最多10组:

对于每组数据:

第一行,包含两个整数n和m,分别表示桌子上有n个盒子以及要执行的操作数量m,1<=n,m<=100000 

接下来m行,每行描述一个操作命令,命令格式如题目描述

输出格式

对于每组数据,输出一行,一个整数,所有奇数位置上的盒子编号之和,位置从左到右编号是1到n。

输入输出样列

输入样例1:

6 4 1 1 4 2 3 5 3 1 6 4 6 3 1 1 4 2 3 5 3 1 6 100000 1 4 

输出样例1:

Case 1: 12 Case 2: 9 Case 3: 2500050000

【耗时限制】1000ms 【内存限制】128MB

思路点拨:

如果用数组来存储盒子,那么对于移动操作,例如把盒子x移动到盒子y左边,那么需要把y及y后面的盒子全部都往后挪,挪动一次时间复杂度是O(n),共m次挪动,可能会超时!

可以利用双向链表的性质,用left[i]和right[i]分别存储编号为i的盒子左边和右边的盒子编号,当进行移动操作时,只需要更改相关联的盒子即可

例如,一开始从左到右盒子的编号一次是1,2,3,...,n,执行编号为9的盒子移动到编号为3的盒子左边

假设我们有如下函数可以让两个结点相互连接

void link(int L,int R)
{
    Right[L]=R;
    Left[R]=L;
}

 对于操作1,假设把9移动到3的左边,X是9,Y是3,那么

 Y=3,left[3]=2,right[3]=4 --> LY=2,RY=4;

X=9,left[9]=8,right[9]=10 --> LX=8,RX=10 

那么依次执行以下操作即可完成移动

link(LX,RX); //实现效果:right[8]=10,left[10]=8

link(LY,X); //实现效果:right[2]=9,left[9]=2

link(X,Y); //实现效果:right[9]=3,left[3]=9

对于操作2,同理假设把3移动到9的右边,X是3,Y是9:

X=3,left[3]=2,right[3]=4 --> LX=2,RX=4; Y=9,left[9]=8,right[9]=10 --> LY=8,RY=10

那么依次执行以下操作即可完成移动:

link(LX,RX); //实现效果:right[2]=4,left[4]=2

link(X,RY); //实现效果:right[3]=10,left[10]=3

link(Y,X); //实现效果:right[9]=3,left[3]=9

对于操作3,假设把3和9进行交换,X是3,Y是9,那么

X=3,left[3]=2,right[3]=4 --> LX=2,RX=4; Y=9,left[9]=8,right[9]=10 --> LY=8,RY=10

那么依次执行以下操作即可完成移动:

link(LX,Y); //实现效果:right[2]=9,left[9]=2

link(Y,RX); //实现效果:right[9]=4,left[4]=9

link(LY,X); //实现效果:right[8]=3,left[3]=8

link(X,RY); //实现效果:right[3]=10,left[10]=3 K13718

对于操作3,如果交换的两个元素本身是相邻的,就会出现自己的左右指针指向自己的情况

假设把3和4进行交换,X是3,Y是4,那么

X=3,left[3]=2,right[3]=4 --> LX=2,RX=4;

那么依次执行以下操作即可完成移动:

link(LX,Y); //实现效果:right[2]=4,left[4]=2

link(Y,X); //实现效果:right[4]=3,left[3]=4

link(X,RY); //实现效果:right[3]=5,left[5]=3 Y=4,left[4]=3,right[4]=5 --> LY=3,RY=5

对于操作4,反转整条链,将last指针和头结点指针对调。但是这样还不够,2、3操作涉及把盒子移动到左边右边,如果反转了,左边右边关系就改变了,如果对每个结点对调左右指针会比较麻烦和费时。如果数据结构上的某一个操作很耗时,有时可以用加标记的方式处理,而不需要真的执行那个操作。 

例如,现在有个链条是a->c->d->b

如果没有反转操作,执行一次 1 a b,就是把a放在b的左边,操作后的顺序是c->d->a->b

如果有反转操作,反转2次 第1次反转,

反转后:b->d->c->a

执行1 a b, 执行后:a->b->d->c;

第2次反转:反转后 c->d->b->a

存在上述反转情况下,与没有反转情况下执行了一次把a移动到b的右边的效果一样,即执行一次2ab。反之进行反转,执行2 a b,再反转,执行结果与执行一次1 a b效果一样。

因此可以得出一个结论,用inv标记反转次数,反转一次就取反,两次反转等于没有反转,并且反转操作只会影响操作1和操作2。只不过inv为1时,op==1时就相当于op==2,op==2时就相当于op==1。所以op=3-op。

利用inv这个变量也有利于最后的输出,最后输出发现inv是0的话,就是没逆转,那么直接把奇数位置的数加起来即可,反之,要用总和减去这个偶数序列(因为当n是偶数并且逆转的情况,sum其实是偶数位置,n是奇数时不影响累加和)

详解代码

#include<iostream>
#include<cstdio>
using namespace std;
int n,Left[100005],Right[100005];
void link(int L,int R)
{
    Right[L]=R;
    Left[R]=L;
}
int main()
{
    int m,k=0;
    while(cin>>n>>m)
	{
        for(int i=1;i<=n;i++)
		{
            Left[i]=i-1;
            Right[i]=i+1;
        }
        Right[0]=1;
        Left[0]=n;
        int op,X,Y,f=0;
        while(m--)
		{
            cin>>op;
            if(op==4) f=!f;
            else
			{
                cin>>X>>Y;
                if(op==3&&Right[Y]==X) swap(X,Y);
                if(op!=3&&f) op=3-op;
                if(op==1&&X==Left[Y]) continue;
                if(op==2&&X==Right[Y]) continue;

                int LX=Left[X],RX=Right[X],LY=Left[Y],RY=Right[Y];
                if(op==1)
				{
                    link(LX,RX);
					link(LY,X);
					link(X,Y);
                }
                else if(op==2)
				{
                    link(LX,RX);
					link(Y,X);
					link(X,RY);
                }
                else if(op==3)
				{
                    if(Right[X]==Y)
					{
                        link(LX,Y);
						link(Y,X);
                        link(X,RY);
                    }
                    else
					{
                        link(LX,Y);
                        link(Y,RX);
                        link(LY,X);
                        link(X,RY);
                    }
                }
            }
        }
        int b=0;
        long long ans=0;
        for(int i=1;i<=n;i++)
		{
            b=Right[b];
            if(i%2==1) ans+=b;
        }
        if(f&&n%2==0) ans=(long long) n*(n+1)/2-ans;
        printf("Case %d: %lld\n",++k,ans);
    }
}

第一次做这么长的文章,望大家多多支持!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值