通过两道半题探讨一下什么是合理的实现

本文探讨了在编程中如何在保持思路一致性的前提下,优化实现技巧,重点关注了T2问题的改进、并查集问题中的不合理实现改进,以及火车调度问题的代码简化。作者提倡合理分类讨论和合并操作,以提高代码效率和美感。

其实就是无题。
这篇博客写出来,纯粹是探讨一个比较玄的问题。做题的方法不一样必然导致实现不一样,因此产生的实现的简单和复杂、常数大小的区别不作任何讨论,这里讨论的主要是在思路一定或者做法一定的情况下,应该怎么优化实现的思路。
在这儿探讨首先带上上篇博客的所谓T2,那个就是一个改实现改的比较成功的例子。
回顾一下这题我的代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cassert>
using namespace std;
const int N = 1e3 + 1;
const int M = 1e5 + 1;
int temp[N][N],A[N][N],stak[M];
int n,cr,cd,top;
char s[M];
void back(){
	int i,j;
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			A[i][j] = temp[i][j];
		}
	}
}
void rightmove(int x){
	int i,j;
	x = x / abs(x) * (abs(x) % n);
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			temp[i][(j + x + n) % n] = A[i][j];
		}
	}
	back();
}
void downmove(int x){
	int i,j;
	x = x / abs(x) * (abs(x) % n);
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			temp[(i + x + n) % n][j] = A[i][j];
		}
	}
	back();
}
void linerev(){
	int i,j;
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			temp[i][A[i][j] - 1] = j + 1;
		}
	}
	back();
}
void columnrev(){
	int i,j;
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			temp[A[i][j] - 1][j] = i + 1;
		}
	}
	back();
}
void linesolve(){
	if(cr) rightmove(cr);
	linerev();
	cr = 0;
}
void columnsolve(){
	if(cd) downmove(cd);
	columnrev();
	cd = 0;
}
int main(){
	freopen("mat.in","r",stdin);
	freopen("mat.out","w",stdout);
	int i,j,q,x;
	bool sub = 0;
	scanf("%d %d",&n,&q);
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			scanf("%d",&A[i][j]);
		}
	}
	scanf("%s",s + 1);
	for(i = 1;i <= q;i++){
		if(s[i] != 'I' && s[i] != 'C'){
			sub = 1;
			break;
		}
	}
	if(sub){
		for(i = 1;i <= q;i++){
			if(s[i] == 'R') ++cr;
			if(s[i] == 'L') --cr;
			if(s[i] == 'D') ++cd;
			if(s[i] == 'U') --cd;	
			if(s[i] == 'I'){
				linesolve();
			}
			if(s[i] == 'C'){
				columnsolve();
			}
		}
		if(cr) rightmove(cr);
		if(cd) downmove(cd);
	}
	else{
		for(i = 1;i <= q;i++){
			if(s[i] == 'I') x = 1;
			else x = 0;
			if(!top || stak[top] != x) stak[++top] = x;
			else --top; 
		}
		for(i = 1;i <= top;i++){
			if(stak[i]) linerev();
			else columnrev();
		}
	}
	for(i = 0;i < n;i++){
		for(j = 0;j < n;j++){
			printf("%d ",A[i][j]);
		}
		printf("\n");
	}
}
/*
3 2
1 2 3
2 3 1
3 1 2
DR
5 4
1 5 2 4 3
3 4 5 1 2
2 1 3 5 4
5 2 4 3 1
4 3 1 2 5
RCDI
*/

思路其实不复杂,如果是LRUD移动操作,计数器记录向右和向下的步数;如果是取逆,先处理对应的水平/竖直移动,然后取逆。另外就是一个分类讨论。
在上一篇博客当中有提到,我放弃了对偶数次连续取逆等于本身(^=1),就是因为不利于实现。如果加上这个,首先其实对时间复杂度贡献不大,ICIC该卡还是得卡;其次为了实现,需要再加两个计数器,而且在取逆操作出现的时候并不能立刻操作(因为后面有可能还有取逆),而是等到出现了对应的水平/竖直移动之后再操作+取逆,且不论这样是否是正确的(以及为了验证正确性还得费时间,又是亏的一方面),实现的时候需要在每一个判断s[i]后面判断是否调用函数,且不能集合起来写,加上取逆的判断(因为如果前面出现了另一种取逆就必须操作了)这样一来大约多了七八个判断,而且会使得solve失去意义,还要多写很多判断。综上所述,把这个性质扔了然后改成特判非常好写,代码上时间亏损不大,考场上时间节省很大,很合理。
此处的solve也完全可以不要,只是为了思路清晰而加。

然后是这么一道题:

已知n个约束条件,分别为x[i],y[i],z[i],z[i]=1表示x[i]等于y[i],z[i]=0表示x[i]不等于y[i],t组数据,对于每组数据,判断所有条件是否能同时满足。x[i],y[i]<=1e9,n<=1e6,t<=10.

此题首先很显然是一个并查集问题,由于x,y比较大,需要离散化(用map,find函数时间复杂度过高)。
在最初实现的时候,考虑用一个vis数组判断一下这个点是否出现在约束条件中过,这样才能判断是否有矛盾。问题就在于这个vis怎么处理,如果是不等关系,只要x,y有一个没出现过的,肯定是无矛盾,但是即使都出现过也不一定(A=B,C=D,A≠C,合法),这说明相等关系不能影响vis,但是一串相等的当中出现不等也不对,加上不等关系也不一定能影响vis,所以必须结合一下x,y是否在同一个集合,相等肯定归为同一集合,不等的时候就要判断两者是否属于同一集合了。然而怎么用vis存储不等关系又是一个问题了…很显然在这个时候,这个实现就已经非常不对劲了,至少我已经完全绕进去了。这个实现就是很不合理的。
最大的麻烦就在于怎么更新vis,很显然相等不更新vis,既然如此,反正并不关心在哪里出现了矛盾,完全可以先处理等后处理不等,处理不等的时候如果在同一集合就是无解,此题就完全没有难度了。
但是这题真的就卡了我足足有一个多小时。这说明,一旦某种if-else+判断实现非常困难,往往说明这个判断根本就不合理,这就应该考虑改变思路。

我个人非常不赞成在某种方法做到一半觉得写起来麻烦的时候换,因为再考虑别的打乱节奏,但是这种时候必须要换思路实现,教训实在是太多了。

最后是这道题:

有一列最大载客量为c的空载火车从1站驶往n站,有k组乘客,第i组有p[i]个人,从x[i]站上车,y[i]站下车,同一组乘客可以不用全部上或下车,可以在必要的时候让未到站的乘客下车,求最多能成功完成多少个乘客的旅程。

这题有一个比较明显的贪心:在某个站,如果客满,未上车的乘客替换掉车上的终点比他靠后的更优。所以可以用一个堆维护车上的乘客,以终点为第一关键字、人数为第二关键字,终点大的在堆顶。但这只是上车,对于下车,仍然以终点为第一关键字,终点小的在堆顶。下车的这一组人的在车上的人数就是对答案的贡献。剩下的模拟就行了。
说起来很容易,但是怎么替换掉车上的人是一个问题。

在此先强调一遍,必须要在第一级以及所有有出队操作的地方的第一位判断队列是否为空!

首先基本要判断这样几个方面:(以下所指的堆都是维护上车状态的)堆非空;堆顶的人的终点不小于当前的站点序号(其实相当于空,这是因为把下车的人出堆很困难,但是去掉这个贡献并判断并不难,这也算是实现优化的一部分);堆顶的人终点比入堆的大;去掉堆顶这一组,剩余载客量仍然不大于入堆人数(不然应该部分移除)。如果这几条都符合,可以去掉堆顶的整组。
另外,对于上面这几个,不满足的时候操作并不相同。如果堆顶的人终点不小于入堆的,或者堆顶的终点小于站点序号,直接把堆用入堆的人填满;否则,如果是去掉这一组车上剩下的人大于入堆人数,那就把入堆的整组人上车,堆顶去掉一部分人使得恰好车上满员;如果为空,只能说明这组人比整个车上的人都多,直接把堆填满。
当然了,如果加上这一组车上的人不超过载客量直接上车就行了。
最初实现AC代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<functional>
#include<utility>
using namespace std;
const int N = 3e5 + 1;
typedef pair<int,pair<int,int> > pr;
priority_queue<pr,vector<pr>,greater<pr> > R;
priority_queue<pr> Q;
struct yjx{
    int p,st,ed;
}a[N];
bool cmp(yjx x,yjx y){
    if(x.st != y.st) return x.st < y.st;
    return x.ed < y.ed;
}
int main(){
    int i,k,n,v,x,y,z,w = 0,temp,left,cnt = 1,res = 0;
    scanf("%d %d %d",&k,&n,&v);
    for(i = 1;i <= k;i++){
        scanf("%d %d %d",&a[i].st,&a[i].ed,&a[i].p);
    }
    sort(a + 1,a + k + 1,cmp);
    for(i = 1;i <= n;i++){
        if(cnt > k && Q.empty()) break;
        while(!R.empty() && R.top().first == i){
            temp = R.top().second.first;
            //printf("%d %d %d?\n",i,temp,a[temp].p);
            res += a[temp].p;
            w -= a[temp].p;
            R.pop();
        } 
        //printf("%d\n",w);
        while(a[cnt].st == i){
            if(a[cnt].p + w > v){
                //printf("%d %d!\n",i,Q.top().second.first);
                while(!Q.empty() && Q.top().first > i && w - Q.top().second.first + a[cnt].p > v && a[cnt].ed < Q.top().first){
                    w -= Q.top().second.first;
                    a[Q.top().second.second].p = 0;
                    //printf("%d?\n",Q.top().second.second);
                    Q.pop();
                }
                if(Q.empty() || a[cnt].ed < Q.top().first && (w - Q.top().second.first + a[cnt].p <= v || Q.top().first <= i)){
                    if(!Q.empty()){
                        left = v - (w - Q.top().second.first) - a[cnt].p;
                        x = Q.top().first;
                    	y = Q.top().second.second;
                    	Q.pop();
						//printf("%d %d %d %d?????\n",left,w,cnt,a[cnt].p);
                    }
        			else left = -1;
                    if(left >= 0){
                    	a[y].p = left;
                        Q.push(make_pair(x,make_pair(left,y)));
                        Q.push(make_pair(a[cnt].ed,make_pair(a[cnt].p,cnt)));
                    }
                    else if(left < 0){
                        //printf("%d\n",i);
                        a[cnt].p = v;
                        a[y].p = 0;
                        Q.push(make_pair(a[cnt].ed,make_pair(v,cnt)));
                    }
                    R.push(make_pair(a[cnt].ed,make_pair(cnt,a[cnt].p)));
                }
                else if(a[cnt].ed >= Q.top().first){
                    Q.push(make_pair(a[cnt].ed,make_pair(v - w,cnt)));
                    a[cnt].p = v - w;
                    //printf("%d %d?????????????????\n",cnt,a[cnt].p);
                    R.push(make_pair(a[cnt].ed,make_pair(cnt,a[cnt].p)));
                } 
                w = v;
            }
            else{
                //printf("%d %d???\n",a[cnt].ed,a[cnt].p);
                w += a[cnt].p;
                Q.push(make_pair(a[cnt].ed,make_pair(a[cnt].p,cnt)));
                R.push(make_pair(a[cnt].ed,make_pair(cnt,a[cnt].p)));
            }
            ++cnt;
        }
    }
    printf("%d\n",res);
    return 0;
}

这个实现就不算太差,但是不要忘了得到这个AC代码之前究竟改了多少…

按照我的习惯这题必须用上pair嵌套,这样写的话非常繁复,而且debug难度极大(反映到实际就是从开始做到改对花了3个半小时,只不过大约有一个小时没有用两个堆,虽然这个时候思路有问题,但是大部分时间还是在改判断的漏洞)。因此要优化实现。
首先,不管哪一种情况,总会有入堆(只不过可能是0个人),入堆完全可以合并。另外,在特殊情况需要计算堆顶的人应该剩下多少人的时候,除非空堆,否则剩下的人一定非负,通过这个就可以完成一次分类讨论(代码当中已经优化了这一点)。以及如果是个空堆,不存在什么堆顶,堆顶不用改了,所以这又可以少写一些东西。
其次维护上车的堆根本不在乎这组的人数,所以可以少一层嵌套。
经过一些修改,删掉注释,得到代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<functional>
#include<utility>
using namespace std;
const int N = 3e5 + 1;
typedef pair<int,pair<int,int> > pr;
typedef pair<int,int> prr;
priority_queue<prr,vector<prr>,greater<prr> > R;
priority_queue<pr> Q;
struct yjx{
    int p,st,ed;
}a[N];
bool cmp(yjx x,yjx y){
    if(x.st != y.st) return x.st < y.st;
    return x.ed < y.ed;
}
int main(){
    int i,k,n,v,x,y,w = 0,left,cnt = 1,res = 0;
    scanf("%d %d %d",&k,&n,&v);
    for(i = 1;i <= k;i++) scanf("%d %d %d",&a[i].st,&a[i].ed,&a[i].p);
    sort(a + 1,a + k + 1,cmp);
    for(i = 1;i <= n;i++){
        if(cnt > k && R.empty()) break;
        while(!R.empty() && R.top().first == i){
            res += a[R.top().second].p;
            w -= a[R.top().second].p;
            R.pop();
        }
        while(a[cnt].st == i){
            if(a[cnt].p + w > v){
                while(!Q.empty() && Q.top().first > i && w - Q.top().second.first + a[cnt].p > v && a[cnt].ed < Q.top().first){
                    w -= Q.top().second.first;
                    a[Q.top().second.second].p = 0;
                    Q.pop();
                }
                if(!Q.empty() && a[cnt].ed >= Q.top().first) a[cnt].p = v - w;                   
                else{
                    if(!Q.empty() && Q.top().first > i){
                        left = v - (w - Q.top().second.first) - a[cnt].p;
                        x = Q.top().first,y = Q.top().second.second;
                    	Q.pop();
                    }
                    else left = -1;
                    if(left >= 0){
                    	a[y].p = left;
                        if(left) Q.push(make_pair(x,make_pair(left,y)));
                    }
                    else a[cnt].p = v;
                }
                w = v;
            }
            else w += a[cnt].p;
            Q.push(make_pair(a[cnt].ed,make_pair(a[cnt].p,cnt)));
            R.push(make_pair(a[cnt].ed,cnt));
            ++cnt;
        }
    }
    printf("%d\n",res);
    return 0;
}

这一来代码就清爽很多了,少了1/4的长度(这是已经忽略了注释的情况),而且对于时间空间没什么影响。这题给我们的启示就是,对于一些分类讨论,尽可能合并不同讨论当中的操作,最好是能去掉一些不合理的分类讨论,这方面活用assert功能。

总归怎么写出更合理的甚至符合(个人)审美的实现是一个长期的习惯性问题,不容易说得清,但是还是有很多可实现的东西,有很多内容值得拿出来探讨探讨,大约以后也会接着有所补充。

Thank you for reading!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值