链表练习:破损的键盘 移动盒子

本文深入探讨了使用数组模拟链表和双向链表在算法竞赛中的优势,通过对比分析和实践案例,展示了如何优化链表操作以提高效率,避免TLE问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天本想给线性数据结构轻松收个尾,刷两道“水题”,但是最后都TLE了,发现了一大堆问题,需要好好总结总结。

两道题分别是破损的键盘移动盒子,乍一看不就分别考察了单链表和双链表嘛(这里怎么根据题目特征分析出考察点就不做赘述,后文都有),前一阵子学校里刚教完链表,做过习题,自己也总结过,刚好拿两道题练手,这不,第一题10分钟搞定,第二题费了1个小时多些也调试成功,还是挺轻松的。但是最后结果却是:TLE了,两道题都TLE了,顿时心态有些炸。其实一开始看到数据量上限为10万,时限为1000ms时内心已经有所顾虑,按照学校里教的定义结构体然后中规中矩的编写链表操作真的过的了嘛,结果还真是过不了。现在终于明白了为什么为什么算法竞赛中极少用“真正”的链式结构,都是用数组模拟的,因为快!下面分别贴上两道题的代码然后简要分析,并特别对移动盒子这道题做一下总结归纳。

 

破损的键盘

分析:

最简单的想法是用数组来保存这段文本,然后用一个变量pos保存“光标位置”。这样,输入一个字符相当于在数组中插入一个字符(需要先把后面的字符全部右移,给新字符腾出位置)。很可惜,这样的代码会超时。因为每输入一个字符都可能会引起大量字符的移动。

解决方案是采用链表,每输入一个字符就把它存起来。设输入字符串是s[1~n],则可以用next[i]表示在当前显示屏中s[i]右边的字符编号(即在s中的下标)。为了方便起见,常常在链表的第一个元素之前放一个虚拟结点,在本题中为假设字符串s的最前面还有一个虚拟的s[0],则next[0]就可以表示显示屏中最左边的字符。再用一个变量cur表示光标位置:即当前光标位于s[cur]的右边。cur=0说明光标位于“虚拟字符”s[0]的右边,即显示屏的最左边。为了移动光标,还需要用一个变量last表示显示屏的最后一个字符是s[last]。

ps:可以看到,我们可以将数组模拟的链表与真正的链表建立一一对应关系。首先我们需要有地方存储Link类的中的两个域,我们分别用s[]数组存储值域、next[]数组存储指针域;其次,模拟中也有头结点,为数组中的第一个元素s[0]+next[0];同样我们也需要cur指针表示光标位置,last指针表示最后一个位置。

下面附上代码:

// UVa11988 Broken Keyboard
// Rujia Liu
#include<cstdio>
#include<cstring>
const int maxn = 100000 + 5;
int last, cur, next[maxn]; // 光标位于cur号字符之后面
char s[maxn];

int main() {
  while(scanf("%s", s+1) == 1) {
    int n = strlen(s+1); // 输入保存在s[1], s[2]...中
    last = cur = 0;
    next[0] = 0;

    for(int i = 1; i <= n; i++) {
      char ch = s[i];
      if(ch == '[') cur = 0;
      else if(ch == ']') cur = last;
      else {
        next[i] = next[cur];
        next[cur] = i;
        if(cur == last) last = i; // 更新“最后一个字符”编号
        cur = i; // 移动光标
      }
    }
    for(int i = next[0]; i != 0; i = next[i])
      printf("%c", s[i]);
    printf("\n");
  }
  return 0;
}

博主简直是对lrj大神佩服的五体投地,写的代码跟雕塑似的,那么的完美,没有一点多余,包括这里的char ch = s[i];其实也有讲头,这一句的作用相当是给变量换个名字,简化代码,你看,如果将s[i]换成s[i][j][k][l][m]是不是效果明显多了呢?本代码建议读者当做模板背诵,实在太妙了。

为了让更熟悉指针实现的链表的读者对于上述代码数组具体是怎么模拟链表的,博主臭不要脸的贴上自己写的TLE的代码给读者做参照(代码是无错的,只不过超时),读者可以将博主的代码和lrj的代码一句句按顺序建立联系,同样是一个模板嘻嘻。

#include<stdio.h>
#include<string.h>
const int maxn = 100005;
char text[maxn];

struct List
{
    char ch;
    List* next;
    List(List* p = NULL) :next(p) {}
    List(char c, List* p = NULL) :ch(c), next(p) {}
};

int main()
{
    while (scanf("%s", text) != EOF)
    {
        List *head, *tail, *curr;
        head = tail = curr = new List();
        for (int i = 0; i < strlen(text); i++)
        {
            if (text[i] == '[')
                curr = head;
            else if (text[i] == ']')
                curr = tail;
            else
            {
                curr->next = new List(text[i], curr->next);
                curr = curr->next;
                if (curr->next == NULL) tail = curr;
            }
        }
        curr = head->next;
        while (curr != NULL)
        {
            printf("%c", curr->ch);
            curr = curr->next;
        }
        puts("");
    }
}

 

移动盒子

分析:

根据前面的经验,如果用数组来保存盒子,肯定会超时,但如果想破损的键盘这道题一样只保存一个next值,似乎又不够,怎么办?

解决办法是采用双向链表:用left[i]和right[i]分别表示编号为i的盒子左边和右边的盒子编号(如果是0,表示不存在),则下面的过程可以让两个结点相互连接:

void link(int L, int R)
{
    right[L] = R; left[R] = L;
}

提示:在双向链表这样的复杂链式结构中,往往会编写一些辅助函数用来设置链接关系。

有了这个代码,可以先记录好操作之前X和Y的两边的结点,然后用link函数按照某种顺序连起来。操作4比较特殊,为了避免一次修改所有元素的指针,此处添加一个标记inv,表示有没有执行过操作4。这样,当op为1和2且inv=1时,只需把op变成3-op即可(这里用到了异或交换变量值的思想,将两个数分别与两数异或的结果异或,可以得到另一个数,对应到这一就是1或2去减1+2,可以得到2或1)。最终输出时要根据inv的值进行不同处理。

提示:如果数据结构上的某一个操作很耗时,有时可以用加标记的方式处理,而不需要真的执行那个操作。但同时,该数据结构的所有其他操作都要考虑这个标记。

// UVa12657 Boxes in a Line
// Rujia Liu
#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn = 100000 + 5;
int n, left[maxn], right[maxn];

inline void link(int L, int R) {
  right[L] = R; left[R] = L;
}

int main() {
  int m, kase = 0;
  while(scanf("%d%d", &n, &m) == 2) {
    for(int i = 1; i <= n; i++) {
      left[i] = i-1;
      right[i] = (i+1) % (n+1);//一致性处理
    }
    right[0] = 1; left[0] = n;//??
    int op, X, Y, inv = 0;

    while(m--) {
      scanf("%d", &op);
      if(op == 4) inv = !inv;
      else {
        scanf("%d%d", &X, &Y);
        if(op == 3 && right[Y] == X) swap(X, Y);
        if(op != 3 && inv) 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(inv && n % 2 == 0) ans = (long long)n*(n+1)/2 - ans;
    printf("Case %d: %lld\n", ++kase, ans);
  }
  return 0;
}

lrj的代码不多说,用心体会(一致性处理实在做的太好了!!!)。可以看出,数组模拟的链式结构,其物理上是连续的,逻辑上则根据指针域的指向关系来确定唯一确定其逻辑位置(即用关系定义位置),希望读者有一个思想转化。下面给出的是博主的第一次TLE代码,常规的中规中矩的双链表操作。 

#include<stdio.h>

struct Link
{
    int value;
    Link* prev;
    Link* next;
    Link(int it = 0, Link* p = NULL, Link* pp = NULL) :value(it), prev(p), next(pp) {}
};


Link *head, *tail, *curr;
int reversed;
long long sum;

void init(int n)
{
    reversed = sum = 0;
    head = new Link();
    tail = new Link();
    head->next = tail; curr = tail->prev = head;
    for (int i = 1; i <= n; i++)
    {
        curr->next = curr->next->prev = new Link(i, curr, curr->next);//curr表示当前位置的前一个,一致性处理
        curr = curr->next;
    }
}

void operate(int m)
{
    int op, x, y;
    for (int i = 0; i < m; i++)
    {
        scanf("%d", &op);
        if (op != 4) scanf("%d%d", &x, &y);
        if (reversed)
            if (op == 1) op = 2;
            else if (op == 2) op = 1;

        if (op == 4) reversed ^= 1;
        else if (op == 3)
        {
            Link *p = NULL, *q = NULL;
            curr = head->next;
            while (curr->next != NULL)
            {
                if (curr->value == x) p = curr;
                if (curr->value == y) q = curr;
                if (p != NULL && q != NULL)
                {
                    int temp = p->value;
                    p->value = q->value;
                    q->value = temp;
                    break;
                }
                curr = curr->next;
            }
        }
        else
        {
            Link *p = NULL, *q = NULL;//注意每个变量前都有个星,为了避免错误,指针类型的变量可以分行定义
            curr = head->next;
            while (curr->next != NULL)
            {
                if (curr->value == x) p = curr;//curr表示当前位置,因为是双链表
                if (curr->value == y) q = curr;
                if (p != NULL && q != NULL && p->next != q)
                {
                    /*删除*/
                    p->prev->next = p->next;
                    p->next->prev = p->prev;
                    /*插入*/
                    if (op == 1)
                    {
                        p->next = q;
                        p->prev = q->prev;
                        q->prev = q->prev->next = p;
                    }
                    else
                    {
                        p->next = q->next;
                        p->prev = q;
                        q->next = q->next->prev = p;
                    }
                    break;
                }
                curr = curr->next;
            }
        }
    }
}

int main()
{
    int n, m, T = 0;
    while (scanf("%d%d", &n, &m) == 2)
    {
        init(n);
        operate(m);
        if (!reversed)
        {
            curr = head->next;
            for(int i = 1; i <= n; i++)
            {
                if (i % 2 == 1) sum += curr->value;
                curr = curr->next;
            }
        }
        else
        {
            curr = tail->prev;
            for (int i = 1; i <= n; i++)
            {
                if (i % 2 == 1) sum += curr->value;
                curr = curr->prev;
            }
        }
        printf("Case %d: %lld\n", ++T, sum);
    }
}

然后博主突然想到用可利用空间表技术改进试试,于是有了下面这样不伦不类的代码:

#include<stdio.h>

struct Link
{
	int value;
	Link* prev;
	Link* next;
	Link(int it = 0, Link* p = NULL, Link* pp = NULL) :value(it), prev(p), next(pp) {}
};

Link *head, *tail, *curr;
Link **freelist;
int reversed;
long long sum;

void init(int n)
{
	reversed = sum = 0;
	head = new Link();
	tail = new Link();
	head->next = tail; curr = tail->prev = head;

	for (int i = 1; i <= n; i++)
	{
		freelist[i]->value = i;
		freelist[i]->prev = curr;
		freelist[i]->next = curr->next;
		curr->next = curr->next->prev = freelist[i];
		curr = curr->next;
	}
}

void operate(int m)
{
	int op, x, y;
	for (int i = 0; i < m; i++)
	{
		scanf("%d", &op);
		if (op != 4) scanf("%d%d", &x, &y);
		if (reversed)
			if (op == 1) op = 2;
			else if (op == 2) op = 1;

		if (op == 4) reversed ^= 1;
		else if (op == 3)
		{
			Link *p = NULL, *q = NULL;
			curr = head->next;
			while (curr->next != NULL)
			{
				if (curr->value == x) p = curr;
				if (curr->value == y) q = curr;
				if (p != NULL && q != NULL)
				{
					int temp = p->value;
					p->value = q->value;
					q->value = temp;
					break;
				}
				curr = curr->next;
			}
		}
		else
		{
			Link *p = NULL, *q = NULL;
			curr = head->next;
			while (curr->next != NULL)
			{
				if (curr->value == x) p = curr;
				if (curr->value == y) q = curr;
				if (p != NULL && q != NULL && p->next != q)
				{
					p->prev->next = p->next;
					p->next->prev = p->prev;
					if (op == 1)
					{
						p->next = q;
						p->prev = q->prev;
						q->prev = q->prev->next = p;
					}
					else
					{
						p->next = q->next;
						p->prev = q;
						q->next = q->next->prev = p;
					}
					break;
				}
				curr = curr->next;
			}
		}
	}
}

int main()
{
	int n, m, T = 0;
	freelist = new Link*[100005];
	for (int i = 0; i < 100005; i++)
		freelist[i] = new Link();
	while (scanf("%d%d", &n, &m) == 2 && n)
	{
		init(n);
		operate(m);
		if (!reversed)
		{
			curr = head->next;
			for (int i = 1; i <= n; i++)
			{
				if (i % 2 == 1) sum += curr->value;
				curr = curr->next;
			}
		}
		else
		{
			curr = tail->prev;
			for (int i = 1; i <= n; i++)
			{
				if (i % 2 == 1) sum += curr->value;
				curr = curr->prev;
			}
		}
		printf("Case %d: %lld\n", ++T, sum);
	}
}

事实证明,这一微小的改进对速度影响还是蛮大的,可以看到第一个程序耗时364ms,第二个程序仅为266ms,而且测试用例角度一点都不刁钻。

测试用例

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
100000 1
4
100000 1
4
100000 1
4
100000 1
4
0 0

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
100000 1
4
100000 1
4
100000 1
4
100000 1
4

0 0
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
100000 1
4
100000 1
4
100000 1
4
100000 1
4
0 0

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
100000 1
4
100000 1
4
100000 1
4
100000 1
4

0 0

测试代码

#include<stdio.h>
#include<time.h>
#include<iostream>

struct Link
{
	int value;
	Link* prev;
	Link* next;
	Link(int it = 0, Link* p = NULL, Link* pp = NULL) :value(it), prev(p), next(pp) {}
};

clock_t start, finish;
void test1()
{
	start = clock();
	int n, m, T = 0;
	Link *head, *tail, *curr;
	int reversed;
	long long sum;
	while (scanf("%d%d", &n, &m) == 2 && n)
	{
		reversed = sum = 0;
		head = new Link();
		tail = new Link();
		head->next = tail; curr = tail->prev = head;
		for (int i = 1; i <= n; i++)
		{
			curr->next = curr->next->prev = new Link(i, curr, curr->next);//curr表示当前位置的前一个,一致性处理
			curr = curr->next;
		}

		int op, x, y;
		for (int i = 0; i < m; i++)
		{
			scanf("%d", &op);
			if (op != 4) scanf("%d%d", &x, &y);
			if (reversed)
				if (op == 1) op = 2;
				else if (op == 2) op = 1;

			if (op == 4) reversed ^= 1;
			else if (op == 3)
			{
				Link *p = NULL, *q = NULL;
				curr = head->next;
				while (curr->next != NULL)
				{
					if (curr->value == x) p = curr;
					if (curr->value == y) q = curr;
					if (p != NULL && q != NULL)
					{
						int temp = p->value;
						p->value = q->value;
						q->value = temp;
						break;
					}
					curr = curr->next;
				}
			}
			else
			{
				Link *p = NULL, *q = NULL;//注意每个变量前都有个星,为了避免错误,指针类型的变量可以分行定义
				curr = head->next;
				while (curr->next != NULL)
				{
					if (curr->value == x) p = curr;//curr表示当前位置,因为是双链表
					if (curr->value == y) q = curr;
					if (p != NULL && q != NULL && p->next != q)
					{
						/*删除*/
						p->prev->next = p->next;
						p->next->prev = p->prev;
						/*插入*/
						if (op == 1)
						{
							p->next = q;
							p->prev = q->prev;
							q->prev = q->prev->next = p;
						}
						else
						{
							p->next = q->next;
							p->prev = q;
							q->next = q->next->prev = p;
						}
						break;
					}
					curr = curr->next;
				}
			}
		}

		if (!reversed)
		{
			curr = head->next;
			for (int i = 1; i <= n; i++)
			{
				if (i % 2 == 1) sum += curr->value;
				curr = curr->next;
			}
		}
		else
		{
			curr = tail->prev;
			for (int i = 1; i <= n; i++)
			{
				if (i % 2 == 1) sum += curr->value;
				curr = curr->prev;
			}
		}
		printf("Case %d: %lld\n", ++T, sum);
	}
	finish = clock();
}

void test2()
{
	start = clock();
	int n, m, T = 0;
	Link *head, *tail, *curr;
	Link **freelist;
	int reversed;
	long long sum;
	freelist = new Link*[100005];
	for (int i = 0; i < 100005; i++)
		freelist[i] = new Link();
	while (scanf("%d%d", &n, &m) == 2 && n)
	{
		reversed = sum = 0;
		head = new Link();
		tail = new Link();
		head->next = tail; curr = tail->prev = head;

		for (int i = 1; i <= n; i++)
		{
			freelist[i]->value = i;
			freelist[i]->prev = curr;
			freelist[i]->next = curr->next;
			curr->next = curr->next->prev = freelist[i];
			curr = curr->next;
		}

		int op, x, y;
		for (int i = 0; i < m; i++)
		{
			scanf("%d", &op);
			if (op != 4) scanf("%d%d", &x, &y);
			if (reversed)
				if (op == 1) op = 2;
				else if (op == 2) op = 1;

			if (op == 4) reversed ^= 1;
			else if (op == 3)
			{
				Link *p = NULL, *q = NULL;
				curr = head->next;
				while (curr->next != NULL)
				{
					if (curr->value == x) p = curr;
					if (curr->value == y) q = curr;
					if (p != NULL && q != NULL)
					{
						int temp = p->value;
						p->value = q->value;
						q->value = temp;
						break;
					}
					curr = curr->next;
				}
			}
			else
			{
				Link *p = NULL, *q = NULL;
				curr = head->next;
				while (curr->next != NULL)
				{
					if (curr->value == x) p = curr;
					if (curr->value == y) q = curr;
					if (p != NULL && q != NULL && p->next != q)
					{
						p->prev->next = p->next;
						p->next->prev = p->prev;
						if (op == 1)
						{
							p->next = q;
							p->prev = q->prev;
							q->prev = q->prev->next = p;
						}
						else
						{
							p->next = q->next;
							p->prev = q;
							q->next = q->next->prev = p;
						}
						break;
					}
					curr = curr->next;
				}
			}
		}

		if (!reversed)
		{
			curr = head->next;
			for (int i = 1; i <= n; i++)
			{
				if (i % 2 == 1) sum += curr->value;
				curr = curr->next;
			}
		}
		else
		{
			curr = tail->prev;
			for (int i = 1; i <= n; i++)
			{
				if (i % 2 == 1) sum += curr->value;
				curr = curr->prev;
			}
		}
		printf("Case %d: %lld\n", ++T, sum);
	}
	finish = clock();
}

int main()
{
	int ok;
	if (scanf("%d", &ok) && ok)
	{
		
		test1();
		printf("\nTime cosume: %lf\n", (double)(finish - start));
		test2();
		printf("\nTime cosume: %lf\n", (double)(finish - start));
	}
	system("pause");
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值