Evanyou Blog 彩带


前面几个代码都是部分分代码,最后一个才是AC了的,所以最后一个有详细注释

这是提高组真题,233有点欧拉回路的感觉


题目大意:

一个 连通 图,双向边无重边 , 访问图中所有点并依次记录经过的点的编号。求记录的编号字典序最小。


题目数据分析

题目中有 M 条边,而 M 有两种情况: M = N 或者 M = N- 1


现在我们将 M = N的情况叫做情况一M = N - 1叫做情况二

情况一和情况二分析

情况二:我们都知道,一棵生成树有N - 1条边,且要保证连通,那么显而易见,情况二是一棵 生成树 。根据生成树的特性,我们可以得知,情况二 不会形成环

情况一:情况一,不过是在情况二上多了一条边。但是还是要保证连通、无重边,故情况二是 在一棵生成树上多了一条表——有且只有一个环

根据题意,可以保证数据 一定 会是以上两种情况。


那么现在我们的问题是如何解决情况一和情况二。

情况二

很简单,一棵生成树,只需要使用爆搜即可。

由于情况二是生成树,所以个人认为DFS更好使用。

那怎么搜?

一颗生成树,是可以以树中任意一个点为 (Root) (根节点)。而题目要求了是要字典序最小,显然 以 1 为根节点开始DFS的答案会更优秀

此时,只需要从 (1)(N) 循环,判断目前循环到的点与当前到达的点是否连通,并再继续DFS即可。

血的教训:由于情况二是遍历一棵树,所以不要回溯!

情况二代码如下:

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 5000 + 10;

int n,m;
bool dis[MAXN][MAXN];
int in[MAXN],ans[MAXN],cnt;
bool b[MAXN];

inline int read(){
    int f = 1, x = 0;
    char c = getchar();

    while (c < '0' || c > '9')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }

    while (c >= '0' && c <= '9')
    {
        x = x * 10 + c - '0';
        c = getchar();
    }

    return f * x;
}

void dfs(int now){
    ans[++cnt] = now;
    b[now] = true;
    for(int i = 1;i <= n; i++){
        if(b[i])continue;
        if(!dis[now][i])continue;
        b[i] = true;
        dfs(i);
    }
}

int main(){
    n = read(),m = read();
    for(int i = 1;i <= m; i++){
        int x = read(),y = read();
        dis[x][y] = dis[y][x] = true;
    }
    dfs(1);
    for(int i = 1;i <= n; i++){
        cout<<ans[i]<<" ";
    }
}

情况二只占60分。那么其他40分就是情况一了

题目都要由简入难。情况二解决,情况一就有思路可想=借鉴了。

现在情况一不过是在情况二上面多了一个环。还可以使用DFS么?很明显不行。

由于所有点只能访问一次(退回来的不算),环中必须会有一条边无法访问。

为什么

因为对于一棵树,dfs是每个节点与其父亲连接的边都可以搜到的;

但是现在多了一条边,又要满足题目要求——每个点除了第一次访问和回溯外,不能再次访问

所以在情况二上多加了一条边,那么一定有一条边无法访问到。

所以呢?

我们只需要找到唯一的环,在环内任意枚举一条边,删去那一条边,再运行情况二的DFS,最后取最优解即可

注意:情况一DFS与情况二DFS不同,情况一由于有环,所以要回溯。

那么我们现在的问题就是要找环。并准确知道哪些点在环内。

如何找环?

并查集?很明显不行。它只能判断两个点是否是一个父亲且一个点在不在环内,很麻烦求出准确的环。

Tarjan?太麻烦了......

那么就用——拓扑排序!

并查集、Tarjan、拓扑排序之后都会在本人Blog中介绍

我们拓扑排序删边时,只要标记当前队列的头,拓扑之后那些没被标记过的,也就是没被拓扑排序的、在环内的点。

但是拓扑排序只有入度为0的点才加入队列么?题目说是无向图,一条边两个点入度都++,那不就没有入度为0的点了么?

没关系,我们只需要将入度和出度加起来当做度数来,再判断度数是不是1即可。

为什么?

因为拓扑排序在累计入度的时候,环的入口入度一定为 (3) ,一个父亲,另外的是环内的点自带的入度。删去与环连接的那一条边,入口点的入度还是会有大于一,故无法进队处理。

那么情况一和情况二的合集代码如下

先不要提交下面的代码!因为没有前面说的注释,所有后面还有没讲完的

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 5000 + 10;

int n,m,cur;
int in[MAXN],ans[MAXN];
int res[MAXN];


vector<int>nei[MAXN];
bool dis[MAXN][MAXN];
struct Line{
    int xx,yy;
}line[MAXN];

bool b[MAXN],use[MAXN];

inline int read(){
    int f = 1, x = 0;
    char c = getchar();

    while (c < '0' || c > '9')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }

    while (c >= '0' && c <= '9')
    {
        x = x * 10 + c - '0';
        c = getchar();
    }

    return f * x;
}

void init(){
    n = read(),m = read();

    for(int i = 1;i <= m; i++){
        int x = read(),y = read();
        dis[x][y] = dis[y][x] = true;
        nei[x].push_back(y);
        nei[y].push_back(x);
        in[x]++,in[y]++;
        line[i] = (Line){x,y};
    }
    return;
}

void out(){
    for(int i = 1;i <= n; i++){
        cout<<ans[i]<<" "; 
    }
    cout<<"
";
    return;
}

void new_ans(){
    for(int i = 1;i <= n; i++){
        ans[i] = res[i];
    }
}

void work() {
    if(ans[1] == 0){
    	new_ans();
    	//out();
    	return;
    }

    for(int i = 1; i <= n; i++) {
        if(res[i] == ans[i])
            continue;
        else if(res[i] < ans[i]) {
            new_ans();
            //out();
            return;
        }
        //out();
        
        return;
    }
}

void Topo(){
    queue<int>q;
    
    for(int i = 1;i <= n; i++){
        if(in[i] == 1){
            q.push(i);
            use[i] = true;
        }
    }
    
    while(!q.empty()){
        int tot = q.front();
        int len = nei[tot].size();
        
        q.pop();
        use[tot] = true;
        
        for(int i = 0;i < len; i++){
            int next = nei[tot][i];
            in[next]--;
            
            if(in[next] == 1){
                q.push(next);
                use[next] = true;
            }
        }
    }
}

void dfs_plan_1(int now){
    ans[++cur] = now;
    b[now] = true;
    for(int i = 1;i <= n; i++){
        if(b[i])continue;
        if(!dis[now][i])continue;
        b[i] = true;
        dfs_plan_1(i);
    }
}

void dfs(int now){
    res[++cur] = now;
    b[now] = true;
    for(int i = 1;i <= n; i++){
        if(b[i])continue;
        if(!dis[now][i])continue;
        b[i] = true;
        dfs(i);
        b[i] = false;
    }
    b[now] = false;
} 

int main(){
    init();
    
    for(int i = 1; i <= n; i++) {
        sort(nei[i].begin(), nei[i].end());
    }
    
    if(m == n - 1){
        dfs_plan_1(1);
        out();
        return 0;
    }

    Topo();

    for(int i = 1;i <= m; i++){
        int x = line[i].xx;
        int y = line[i].yy;

        if(use[x] || use[y])continue;

        dis[x][y] = dis[y][x] = false;
        cur = 0;

        dfs(1);

        work();
        dis[x][y] = dis[y][x] = true;
    }
    
    out();
    return 0;
}

为什么TLE了几个点?

不是叫你别提交么

因为这种做法枚举了所有点,会导致超时,所以我们现在只需要维护每个点和它相邻的边,枚举那些边就好。


那么现在思路非常明确了:

那么其实上面就是一个重要的知识——

基环图

没完,代码下面还有东西

#include<bits/stdc++.h>//万能头 
using namespace std;

const int MAXN = 5000 + 10;//定义常量 

int n,m,cur;//n、m与题目相对应,cur记录答案累加用途 
int in_out[MAXN],ans[MAXN];//ans为答案,in_out为度数(即入读加上出度) 
int res[MAXN];//用于临时存目前计划,在更新ans与res判断 


vector<int>nei[MAXN];//动态数组用于记录邻居,这里用于优化时间复杂度 
bool dis[MAXN][MAXN];//邻接矩阵,用来优化判断两个点的边目前又没有删 
struct Line{//记录边的结构体 
	int xx,yy;//起始点和终止点 
}line[MAXN];

bool b[MAXN],use[MAXN];//b用于在DFS时看是否搜过了 ,use记录该店是否在环内 

inline int read(){//读入优化 
    int f = 1, x = 0;
    char c = getchar();

    while (c < '0' || c > '9')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }

    while (c >= '0' && c <= '9')
    {
        x = x * 10 + c - '0';
        c = getchar();
    }

    return f * x;
}

void init(){//读入函数 
	n = read(),m = read();//读入点和边 

	for(int i = 1;i <= m; i++){//读入边 
		int x = read(),y = read();
		dis[x][y] = dis[y][x] = true;//将x与y的邻接矩阵更新 
		nei[x].push_back(y);//将y压入x的邻居 
		nei[y].push_back(x);//将x压入y的邻居
		in_out[x]++,in_out[y]++;//将x和y的度数++ 
		line[i] = (Line){x,y};//保存边 
	}
	
	//for(int i = 1;i <= n; i++){
	//	for(int j = 1;j <= n; j++){
	//		cout<<dis[i][j]<<" ";
	//	}
	//	cout<<endl;
	//}
	//调试函数 
	
	return;
}

void out(){//输出函数,单独拿出来是用于调试用的 
	for(int i = 1;i <= n; i++){
		cout<<ans[i]<<" "; //循环输出答案 
	}
	cout<<"
";
	return;
}

void new_ans(){//更新答案 
	for(int i = 1;i <= n; i++){
		ans[i] = res[i];
	}
}

void work() {//用于判断目前方案与答案哪个更优 
    if(ans[1] == 0){//如果目前答案为空,直接记录 
    	new_ans();//更新答案 
    	//out();
    	return;//这里一定要返回 
    }

    for(int i = 1; i <= n; i++) {//逐次比较哪个更优 
        if(res[i] == ans[i])//如果相同则继续 
            continue;
        else if(res[i] < ans[i]) {//当前方案更优 
            new_ans();//更新 
            //out();
            return;
        }
	//	out();
		
        return;
    }
}

void Topo(){//基环图(与拓扑排序差不多) 
	queue<int>q;//定义队列 
	
	for(int i = 1;i <= n; i++){//将度数为1的雅图队列 
		if(in_out[i] == 1){
			q.push(i);
			use[i] = true;//标记不在环中 
		}
	}
	
	while(!q.empty()){//还可以继续做 
		int tot = q.front();//取队首 
		int len = nei[tot].size();
		
		q.pop();
		use[tot] = true;//标记不在环中 
		
		for(int i = 0;i < len; i++){//依次删除度数 
			int next = nei[tot][i];//下一个点 
			in_out[next]--;
			
			if(in_out[next] == 1){//压入队列 
				q.push(next);
				use[next] = true;
			}
		}
	}
}

void dfs_plan_1(int now){//用于情况二的DFS,这里不要回溯 
	ans[++cur] = now;//直接更新答案 
    b[now] = true;//标记 
    int len = nei[now].size(); 
    
    for(int i = 0;i < len; i++){//循环
		int next = nei[now][i];
	 
        if(b[next])continue;
        if(!dis[now][next])continue;
        b[next] = true;
        dfs_plan_1(next);
    }
}

void dfs(int now){//用于情况一的DFS,这里要回溯 
	res[++cur] = now;//更新当前方案 
	b[now] = true;
	int len = nei[now].size();	
	for(int i = 0;i < len; i++){
		int next = nei[now][i];
		
		if(b[next])continue;
		//cout<<now<<" "<<next<<" "<<dis[i][next]<<endl;
		if(!dis[now][next])continue;
		b[next] = true;
		//cout<<next;				
		dfs(next);
		b[next] = false;
	}
	b[now] = false;
	//cout<<"end."<<endl;
} 

int main(){
	init();//读入 
	
	for(int i = 1; i <= n; i++) {
		//这里很巧妙,从小到大排列邻居,优化 
        sort(nei[i].begin(), nei[i].end());
    }
    
	if(m == n - 1){//情况二 
		dfs_plan_1(1);
		out();
		return 0;
	}

	//情况一 

	Topo();//基环图 

	for(int i = 1;i <= m; i++){//枚举删边 
		int x = line[i].xx;
		int y = line[i].yy;

		if(use[x] || use[y])continue;//有一个点不在环中就跳过 

		//cout<<i<<"line:"<<x<<" "<<y<<" "<<dis[x][y]<<endl;

		dis[x][y] = dis[y][x] = false;//删边 
		cur = 0;

		dfs(1);//DFS 

		work();//更新答案 
		dis[x][y] = dis[y][x] = true;//恢复边 
	}
	
	out();//输出 
	return 0;
}

基环图的处理方法

来都来了,听听吧

基换图如上面说的,就是一颗树多了一条边,形成了唯一的环。处理方法也会像上面说的,Topo排序找环、DFS即可

原文地址:https://www.cnblogs.com/CJYBlog/p/LG5022.html