神祕國度的愛情故事 數據結構課設-廣州大學

神祕國度的愛情故事

數據結構課設-廣州大學

ps:本次課設程序不僅需要解決問題,更需要注重代碼和算法的優化和數據測試分析

     直接廣度優先實現的方法時間複雜度爲O(QN),優化後的方法是lca+倍增思想,時間複雜度爲O(Qln(N))

    是自己寫的隨機生成樹和隨機生成詢問,輸入數據保證準確,進行數據測試分析。

     因爲在博客中不在方便調整排版,所以在這裏給出word文檔的下載地址(內容和這次博客的內容完全一致)

    此外,還有一個壓縮包,包括4個cpp源文件,21組測試樣例,以及上面那個完整的word文檔,下載地址

 

 

 

三、實驗內容

神祕國度的愛情故事(難度係數 1.5)

題目要求:某個太空神祕國度中有很多美麗的小村,從太空中可以想見,小村間有路相連,更精確一點說,任意兩村之間有且僅有一條路徑。小村 A 中有位年輕人愛上了自己村裏的美麗姑娘。每天早晨,姑娘都會去小村 B 裏的麪包房工作,傍晚 6 點回到家。年輕人終於決定要向姑娘表白,他打算在小村 C 等着姑娘路過的時候把愛慕說出來。問題是,他不能確定小村 C 是否在小村 B 到小村 A 之間的路徑上。你可以幫他解決這個問題嗎?

輸入要求:輸入由若干組測試數據組成。每組數據的第 1 行包含一正整數 N ( l 《 N 《 50000 ) , 代表神祕國度中小村的個數,每個小村即從0到 N - l 編號。接下來有 N -1 行輸入,每行包含一條雙向道路的兩個端點小村的編號,中間用空格分開。之後一行包含一正整數 M ( l 《 M 《 500000 ) ,代表着該組測試問題的個數。接下來 M 行,每行給出 A 、 B 、 C 三個小村的編號,中間用空格分開。當 N 爲 O 時,表示全部測試結束,不要對該數據做任何處理。

輸出要求:對每一組測試給定的 A 、 B 、C,在一行裏輸出答案,即:如果 C 在 A 和 B 之間的路徑上,輸出 Yes ,否則輸出 No。

思路:在這個課設的題目中,非常巧妙的提出了“任意兩村之間有且僅有一條路徑”,我們可以想象的到,這是n個結點且剛好有n-1條邊的連通圖,以任意一個結點爲根結點進行廣度優先遍歷,便會生成一棵樹。所以我們處理的方法就是對一棵樹的任意兩個結點求他們的最近公共祖先(lca)。這裏我定義了兩個結構體struct Edgestruct  NodeEdge爲兩個結點(村子)之間的相連的邊,Node爲結點(村子)。Node有兩個變量adjnode和next,adjnode爲結點向量中的相鄰結點,next爲指向下一鄰接邊的指針。Node有村子的編號信息,bfs遍歷時的父親結點,深度信息。用鄰接表的方式存儲每一個結點與之相連第一條邊,然後每插入一個與之相連的邊的時候,都會用頭插法的方式更新鄰接表。通過BFS預處理出這棵樹的結點深度和父親結點的信息。在查詢結點a和結點b之間的最近公共祖先的時候,我們可以先把結點深度比較大的結點開始往上一個一個單位的跳,然後跳到和另外一個結點同樣的深度的時候停下來,查看這時候它們是否在同一一個結點上了,如果是,那這個結點就是它們的最近公共祖先了,不是的話,我們這次就兩個結點一起往它們的父親結點那裏一個一個單位的跳,直到第一次跳到相同的結點的地方,這時,該結點便是它們的最近公共祖先。通過課設的題目我們可以知道,任意兩個結點之間必定存在且只有一條路徑互達,所以這樣處理的方法必定可以找到這兩個結點的最近公共祖先。那麼如何解決C點是否在A和B的路徑上呢?我們可以先找出

A和B的最近公共祖先爲D,A和C的最近公共祖先爲AC,B和C的最近公共祖先爲BC。如果AC==C並且BC==C,則說明C同時是A和B的最近公共祖先,這裏需要分情況討論,如果C==D的話,則說明C就是A和B的最近公共祖先,如果C!=D,則說明C不是A和B的最近公共祖先,則A到D再走到B的路徑中,不會經過C結點。如果只有AC==C或者BC==C,則說明C是A或者B中一個且只有一個結點的祖先結點。如果C是A的祖先結點,不是B的祖先結點,則說明C在A和D的路徑上,則C肯定是在A和B的路徑上。如果C是B的祖先結點,不是A的祖先結點,則說明C在B和D的路徑上,則C肯定是在A和B的路徑上。如果C不是A和B中任意一個結點的祖先結點,那麼從A到B的路徑上不會經過C結點。

以下是未優化過的程序的:

時間複雜度爲O(QN),其中,Q爲詢問次數,N爲結點(村子)的數量。(byd001.cpp)

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 50010;
const int DEG = 20;
int edgenum, nodenum;
struct Edge {           //邊的鄰接點
	int adjnode;        //鄰接點在結點向量中的下表
	Edge *next;         //指向下一鄰接邊的指針
};
struct Node {          //結點結點(村子)
	int id;            //結點信息(村子編號)
	int fa;            //結點的父親結點 
	int depth;         //結點的深度 
	Edge *firstarc;    //指向第一個鄰接邊的指針
};
void creatgraph(Node* node, int n) { //創建一個村落圖
	Edge *p;
	int u, v;
	nodenum = n;        //結點(村莊)的個數 
	edgenum = n - 1;    //邊數是結點數-1
	for (int i = 0; i<nodenum; i++) {
		node[i].id = i;              //每個小村從0到N-1編號
		node[i].firstarc = NULL;     //每一個村莊的第一個鄰接邊的指針初始化爲NULL 
	}
	//接下來有n-1行輸入,每行包含一條雙向的兩個端點小村的編號,中間用空格分開。
	//cout << "請輸入村子的" << n - 1 << "條路徑(兩個端點中間用空格分隔):" << endl;
	for (int i = 1; i <= edgenum; i++) {
		cin >> u >> v;
		p = new Edge;         //下面完成鄰接表的建立
		p->adjnode = v;
		p->next = node[u].firstarc;
		node[u].firstarc = p; //類似於頭插法
		p = new Edge;
		p->adjnode = u;
		p->next = node[v].firstarc;
		node[v].firstarc = p; //路徑是雙向的
	}
}
void BFS(Node* &node, int root) {        //bfs廣度優先遍歷,預處理出每一個結點的深度和父親結點 
	queue<int>que;                      //隊列 
	node[root].depth = 0;               //樹的根結點的深度爲0 
	node[root].fa = root;                //樹的根結點的父親結點爲他自己 
	que.push(root);                     //根結點入隊 
	while (!que.empty()) {
		int u = que.front();            //將隊列的頭結點u出隊 
		que.pop();
		Edge* p;
		for (p = node[u].firstarc; p != NULL; p = p->next) {    //找出和u相連的邊 
			int v = p->adjnode;                                  //v爲對應鄰接邊的鄰接結點 
			if (v == node[u].fa) continue;                      //因爲存儲的是雙向邊,所以防止再訪問到已經訪問過的父親結點 
			node[v].depth = node[u].depth + 1;                  //結點的深度爲父親結點的深度+1
			node[v].fa = u;                                      //記錄v結點的父親結點爲u 
			que.push(v);                                        //v結點入隊 
		}
	}
}
int LCA(Node* node, int u, int v) {                              //在樹node中,找出結點u和結點v的最近公共祖先 
	if (node[u].depth > node[v].depth)swap(u, v);               //u爲深度較小的結點,v爲深度較大的結點 
	int hu = node[u].depth, hv = node[v].depth;
	int tu = u, tv = v;
	for (int det = hv - hu, i = 1; i <= det; i++)                 //兩個結點的深度差爲det,結點v先往上跑det個長度,使得這兩個結點在同一深度 
		tv = node[tv].fa;
	if (tu == tv)return tu;                                     //如果他們在同一深度的時候,在同一結點了,那麼這個結點就是這兩個結點的最近公共祖先 
	while (tu != tv) {                                             //他們不在同一結點,卻在同一深度了,那就兩個結點一起往上跳一個單位
		tu = node[tu].fa;                                       //直到跳到同一個結點,那這個結點就是它們的最近公共祖先 
		tv = node[tv].fa;
	}
	return tu;                                                  //返回最近公共祖先 
}
void solve(Node* node, int a, int b, int c) {                       //在樹node中,查詢結點c是否在a和b的路徑上 
	int d = LCA(node, a, b);                                         //找出a和b結點的最近公共祖先爲d 
	int ac = LCA(node, a, c);                                        //找出a和c結點的最近公共祖先爲ac 
	int bc = LCA(node, b, c);                                        //找出b和c結點的最近公共祖先爲bc
	//cout<<d<<" "<<ac<<" "<<bc<<endl;
	if (ac == c&&bc == c) {                                           //如果ac==c並且bc==c,說明c結點是a和b結點的公共祖先 
		if (c == d) {                                               //如果c==d,說明c就是a和b的最近公共祖先,c必定在a和b的路徑上 
			cout << "Yes" << endl;
		}
		else
			cout << "No" << endl;                                    //如果c!=d,說明c不是a和b的最近公共祖先,a和b的路徑上不包括c 
	}
	else if (ac == c || bc == c) {                                    //c是a的祖先或者是b的祖先,說明c在a到d的路徑上或者在b到d的路徑上 
		cout << "Yes" << endl;                                       //此時c一定是a和b路徑上的點 
	}
	else {
		cout << "No" << endl;                                        //如果c不是a的祖先,也不是b的祖先,則a和b的路徑上不會經過c點 
	}
}
int main() {
#ifndef OnLINE_JUGE
	//freopen("D:\\test_in.txt","r",stdin);                        //數據輸入爲測試數據文件test_in.txt的內容 
	//freopen("D:\\test_out_1.txt","w",stdout);                    //數據輸出到結果文件test_out_1.txt中 
#endif
	int T, n, m;                              //T爲測試樣例個數,每一組測試樣例包括n個村子,n-1個村子之間的關係,m組詢問            
	int a, b, c;                              //每組詢問包括三個結點編號a,b和c,意思爲詢問c結點是否在a和b結點的路徑上 
	cin >> T;                                 //輸入測試樣例數T 
	while (T--) {
		cin >> n;                             //輸入結點數n 
		Node *node = new Node[n + 1];         //動態申請n+1個結點空間 
		creatgraph(node, n);                 //建樹 
		BFS(node, 0);                        //bfs預處理出樹結點的深度和對應的父親結點 
		cin >> m;                             //輸入m個詢問                
		while (m--) {
			cin >> a >> b >> c;                   //輸入結點編號a,b和c 
			solve(node, a, b, c);              //詢問c結點是否在a和b結點的路徑上
		}
		delete node;                        //這組測試樣例處理結束,撤銷申請的空間 
	}
	return 0;
}

未優化過的程序,在查找最近公共祖先的時候是一個一個往上面查找的,所以在最壞的情況下,時間複雜度會退化到O(QN),比如說在樹退化成單鏈表的時候就會時間複雜度就會很高。優化的策略是按照倍增思想進行向上查找,比如說,一個結點需要跳到它的第7個祖先結點那裏,一次一次的往上跳躍需要7次(如圖001所示),如果把7轉換成二進制111,那就是需要往上跳躍三次,第一次跳躍4個單位,第二次跳躍2個單位,第三次跳躍1個單位就可以跳躍到第7個祖先結點那裏(如圖002所示)。

 

圖001                                                                                        圖002

一開始兩個結點不處於相同的深度的時候,先讓深度較大的用這種方法進行往上的跳躍,使其結點達到相同的深度。然後開始不斷從大的二進制那裏進行試探跳躍,比如說,A現在和B在同一深度了,A和B先試探的跳躍到2^2個父親結點處(如圖003所示),此時他們跳到相同的結點地方了,這個結點就是他們的公共祖先,但不清楚是不是最近公共祖先,所以我們捨棄這一次跳躍,試探跳躍到它們對應第2^1個父親結點(如圖004所示),發現這是它們的結點不一樣,說明它們距離最近的公共祖先又近了一步,下一步就是跳躍到它們對應的第2^0個結點處(如圖005所示),這時i==0,這是循環結束,它們已經跳躍到最接近公共祖先的地方了(如圖006所示)(即此時的父親結點就是最近公共祖先),這時就是它們的最近公共祖先了,成功在log(n)的時間內找到A和B的最近公共祖先,優化了查詢速度。

 

                  圖003                                                                     圖004

 

              圖005                                          圖006

以下爲優化查詢最近公共祖先的方式後的代碼:

(時間複雜度爲O(Qlog(N)),其中Q爲查詢的次數,N爲樹的結點的個數)

(byd004.cpp)

#include<iostream>
#include<cstring>
#include<queue>
#include<windows.h> 
using namespace std;
const int maxn = 50010;
const int DEG = 20;
int edgenum, nodenum;
struct Edge {         //邊的鄰接點
	int adjnode;      //鄰接點在結點向量中的下表
	Edge *next;       //指向下一鄰接點的指針
};
struct Node {        //結點(村子)
	int id;          //結點信息(村子編號)
	int fa[20];      //結點的父親結點,fa[i]表示的是該結點的第2^i個父親結點
	int depth;       //結點的深度
	Edge *firstarc;  //指向第一鄰接點的指針
};
void creatgraph(Node* node, int n) {     //創建一個村落圖
	Edge *p;
	int u, v;
	nodenum = n;                         //結點(村莊)的個數
	edgenum = n - 1;                     //邊數是結點數-1
	for (int i = 0; i<nodenum; i++) {
		node[i].id = i;                  //每個小村從0到N-1編號
		node[i].firstarc = NULL;         //每一個村莊的第一個鄰接邊的指針初始化爲NULL
	}
	//接下來有N-1行輸入,每行包含一條雙向的兩個端點小村的編號,中間用空格分開。
	cout << "請輸入村子的" << n - 1 << "條路徑(兩個端點中間用空格分隔:" << endl;
	for (int i = 1; i <= edgenum; i++) {
		cin >> u >> v;
		p = new Edge;               //下面完成鄰接表的建立
		p->adjnode = v;
		p->next = node[u].firstarc;
		node[u].firstarc = p;       //類似於頭插法
		p = new Edge;
		p->adjnode = u;
		p->next = node[v].firstarc;
		node[v].firstarc = p;       //路徑是雙向的
	}
}
void BFS(Node* &node, int root) {    //bfs廣度優先遍歷,預處理出每一個結點的深度和和對應的第2^i個父親結點
	queue<int>que;                  //隊列
	node[root].depth = 0;           //樹的根結點的深度爲0
	node[root].fa[0] = root;        //樹的根結點的第2^0(第一)個父親結點爲他自己
	que.push(root);                 //根結點入隊
	while (!que.empty()) {
		int u = que.front();        //將隊列的頭結點u出隊
		que.pop();
		for (int i = 1; i < DEG; i++)    //u的第2^i個父親結點等於u的第2^(i-1)個父親結點的第2^(i-1)個父親結點
			node[u].fa[i] = node[node[u].fa[i - 1]].fa[i - 1];
		Edge* p;
		for (p = node[u].firstarc; p != NULL; p = p->next) {    //找出和u相連的邊
			int v = p->adjnode;                                 //v爲對應鄰接邊的鄰接結點
			if (v == node[u].fa[0]) continue;                   //因爲存儲的是雙向邊,所以防止再訪問到已經訪問過的父親結點
			node[v].depth = node[u].depth + 1;                  //結點的深度爲父親結點的深度+1
			node[v].fa[0] = u;                                  //記錄v結點的父親結點爲u
			que.push(v);                                        //v結點入隊
		}
	}
}
int LCA(Node* node, int u, int v) {                          //在樹node中,找出結點u和結點v的最近公共祖先
	if (node[u].depth > node[v].depth)swap(u, v);           //u爲深度較小的結點,v爲深度較大的結點
	int hu = node[u].depth, hv = node[v].depth;
	int tu = u, tv = v;
	for (int det = hv - hu, i = 0; det; det >>= 1, i++)    //兩個結點的深度差爲det,結點v先往上跑det個長度,使得這兩個結點在同一深度
	if (det & 1)                                       //將深度差拆分成二進制進行結點的2^i跳躍,優化了之前的一個一個跳躍的方法
		tv = node[tv].fa[i];
	if (tu == tv)return tu;                                //如果他們在同一深度的時候,在同一結點了,那麼這個結點就是這兩個結點的最近公共祖先
	for (int i = DEG - 1; i >= 0; i--) {                    //他們不在同一結點,卻在同一深度了,那就兩個結點一起往上跳2^i單位
		if (node[tu].fa[i] == node[tv].fa[i])                 //如果會跳過頭了,則跳過這一步跳躍
			continue;
		tu = node[tu].fa[i];
		tv = node[tv].fa[i];
	}
	return node[tu].fa[0];                                  //循環結束後,這兩個結點在同一深度,並且差一個單位就會跳躍到同一個結點上,
}                                                           //則它們結點的第一個父親結點就是它們的最近公共祖先
void solve(Node* node, int a, int b, int c) {                  //在樹node中,查詢結點c是否在a和b的路徑上
	int d = LCA(node, a, b);                                    //找出a和b結點的最近公共祖先爲d
	int ac = LCA(node, a, c);                                   //找出a和c結點的最近公共祖先爲ac
	int bc = LCA(node, b, c);                                   //找出b和c結點的最近公共祖先爲bc
	bool flag = false;
	if (ac == c&&bc == c) {                                      //如果ac==c並且bc==c,說明c結點是a和b結點的公共祖先
		if (c == d) {                                          //如果c==d,說明c就是a和b的最近公共祖先,c必定在a和b的路徑上
			flag = true;
		}
		else
			flag = false;                                     //如果c!=d,說明c不是a和b的最近公共祖先,a和b的路徑上不包括c
	}
	else if (ac == c || bc == c) {                               //c是a的祖先或者是b的祖先,說明c在a到d的路徑上或者在b到d的路徑上
		flag = true;                                          //此時c一定是a和b路徑上的點
	}
	else {
		flag = false;                                         //如果c不是a的祖先,也不是b的祖先,則a和b的路徑上不會經過c點
	}
	if (flag)
		cout << "村子" << c << "在村子" << a << "和村子" << b << "的路徑上" << endl;
	else
		cout << "村子" << c << "不在村子" << a << "和村子" << b << "的路徑上" << endl;
}
int main() {
	//DWORD start_time = GetTickCount();
	int n, m;                            //一組測試樣例包括n個村子,n-1個村子之間的關係,m組詢問
	int a, b, c;                          //每組詢問包括三個結點編號a,b和c,意思爲詢問c結點是否在a和b結點的路徑上
	cin >> n;                             //輸入結點數n
	Node *node = new Node[n + 1];         //動態申請n+1個結點空間
	creatgraph(node, n);                 //建樹
	BFS(node, 0);                        //bfs預處理出樹結點的深度和對應的父親結點
	cin >> m;                             //輸入m個詢問
	while (m--) {
		cin >> a >> b >> c;                   //輸入結點編號a,b和c
		solve(node, a, b, c);              //詢問c結點是否在a和b結點的路徑上
	}
	delete node;                        //這組測試樣例處理結束,撤銷申請的空間
	//DWORD end_time = GetTickCount();
	//cout << "該組測試樣例的用時爲:" << (end_time - start_time) << "ms!" << endl;
	return 0;
}

 

手動輸入一組測試樣例:


11

0 1

0 2                                

0 3

1 4

1 6

2 7

4 5

7 8

7 9

9 10

6

5 8 7

8 9 2

1 3 2

10 8 7

9 2 10

0 10 2

程序運行的結果爲:

 

四,程序驗證

優化後的程序是否真的比未優化的程序優秀呢?

1.我們先隨機生成一顆N個結點的樹和Q次詢問,保證生成的樹是合法的和詢問是合法的。

思路是:先生成一個隨機亂序的0~N-1組成的數組放入隊列q中,隊列q存儲的是爲匹配的結點,隊列que存儲的是已經即將要匹配的結點。比如說,隨機生成一個亂序數組爲5,3,2,4,1,0。隊列q中5先出列放入卻中。1.Que取出隊首元素u,然後隨機生成一個tmp表示u有tmp個分支結點,然後取隊列中tmp個數,隊列中的前tmp個數再取出來放入que中,回到1繼續執行,直到q的隊列爲空,則生成一顆隨機樹成立。(byd003.cpp)

#include<iostream>
#include <cstdio>
#include <queue>
#include<ctime>
#include<algorithm>
using namespace std;
#define ll long long
const int maxn = 250000 + 10;
const int inf = int(1e9) + 7;
const int mod = 1000000007;
int top = 1;
queue<int>q;
queue<int>que;
void init(int n) {    //隨機生成0~n-1的亂序數組,存入隊列q中 
	while (!q.empty())
		q.pop();
	int num[maxn];
	for (int i = 0; i<n; i++)
		num[i] = i;
	time_t t;
	srand((unsigned)time(&t));
	for (int i = 0; i<n; i++) {
		int tmp = (rand() + top) % n;
		//cout << tmp << endl;
		int t = num[i];
		num[i] = num[tmp];
		num[tmp] = t;
	}
	for (int i = 0; i<n; i++) {
		q.push(num[i]);
		//cout <<num[i]<<" ";
	}
	//cout<<endl;
}
void greattree(int n) {      //隨機生成n個結點的樹,和n-1條邊的結點之間的關係 
	cout << n << endl;
	time_t t;
	srand((unsigned)time(&t));
	int u = q.front();
	q.pop();
	que.push(u);
	n--;
	while (!que.empty()) {
		u = que.front();
		que.pop();
		int tmp = (rand() + top) % n;    //分支情況完全隨機的情況下 
		tmp = tmp % 2;    //退化成單鏈表的情況下 
		if (tmp == 0 && que.empty())
			tmp++;
		for (int i = 1; i <= tmp; i++) {
			if (q.empty())
				break;
			int v = q.front();
			q.pop();
			que.push(v);
			cout << u << " " << v << endl;
		}
	}
}
void greatequery(int n) {           //隨機生成tmp組查詢數據,保證a!=b,b!=c,a!=c 
	time_t t;
	srand((unsigned)time(&t));
	int tmp = (rand() + top) % n + 100;
	cout << tmp << endl;
	for (int i = 1; i <= tmp; i++) {
		int a = (rand() + top) % n;
		int b = (rand() + top) % n;
		int c = (rand() + top) % n;
		if (a == b || b == c || a == c){
			i--;
			continue;
		}
		cout << a << " " << b << " " << c << endl;
	}
}
int main() {
#ifndef OnLINE_JUGE
	freopen("D:\\test_in.txt", "w", stdout);   //將生成的隨機樹和隨機詢問寫入測試樣例文件test_in.txt中 
#endif
	int T, n;
	cin >> T >> n;        //生成具有T組測試樣例,每組測試樣例有n個結點的樹 
	cout << T << endl;
	top = 0;
	for (int i = 1; i <= T; i++) {
		top++;               //全局變量top不斷變化,和時間種子相結合,保證每一組隨機生成的數據都不一樣 
		init(n);             //隨機生成0~n-1的亂序數組,存入隊列q中
		greattree(n);        //隨機生成n個結點的樹,和n-1條邊的結點之間的關係 
		greatequery(n);      //隨機生成tmp組查詢數據,保證a!=b,b!=c,a!=c 
	}
	return 0;
}

這樣我們就可以隨機生成樹和查詢數據來測試程序的運行效率了。

這裏生成21個隨機數據測試文件txt如下所示:(測試文件txt也放在壓縮包下的測試數據文件中)

Ps:每個文件下都是十組測試數據

以下爲n個樹結點500個查詢(查詢數先固定)

測試文件test_in1_0.txt  (結點數:10)

測試文件test_in1_1.txt  (結點數:20)

測試文件test_in1_2.txt  (結點數:50)

測試文件test_in1_3.txt  (結點數:100)

測試文件test_in1_4.txt  (結點數:500)

測試文件test_in1_5.txt  (結點數:1000)

測試文件test_in1_6.txt  (結點數:2000)

測試文件test_in1_7.txt  (結點數:5000)

測試文件test_in1_8.txt  (結點數:10000)

測試文件test_in1_9.txt  (結點數:50000)

以下爲50000個樹結點Q個查詢(結點數先固定)

測試文件test_in2_0.txt  (查詢數:10)

測試文件test_in2_1.txt  (查詢數:50)

測試文件test_in2_2.txt  (查詢數:100)

測試文件test_in2_3.txt  (查詢數:500)

測試文件test_in2_4.txt  (查詢數:1000)

測試文件test_in2_5.txt  (查詢數:5000)

測試文件test_in2_6.txt  (查詢數:10000)

測試文件test_in2_7.txt  (查詢數:50000)

測試文件test_in2_8.txt  (查詢數:100000)

測試文件test_in2_9.txt  (查詢數:250000)

一組特殊的測試樣例 test_in0(10組測試樣例,每一組測試樣例有50000,個結點,10000個詢問,生成的樹退化成單鏈表的情況下測試程序的運行時間)

 

優化後的能讀寫txt文件的程序如下所示(byd002.cpp

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 50010;
const int DEG = 20;
int edgenum, nodenum;
struct Edge {         //邊的鄰接點
	int adjnode;      //鄰接點在結點向量中的下表
	Edge *next;       //指向下一鄰接點的指針
};

struct Node {        //結點(村子)
	int id;          //結點信息(村子編號)
	int fa[20];      //結點的父親結點,fa[i]表示的是該結點的第2^i個父親結點 
	int depth;       //結點的深度 
	Edge *firstarc;  //指向第一鄰接點的指針
};
void creatgraph(Node* node, int n) {     //創建一個村落圖
	Edge *p;
	int u, v;
	nodenum = n;                         //結點(村莊)的個數
	edgenum = n - 1;                     //邊數是結點數-1
	for (int i = 0; i<nodenum; i++) {
		node[i].id = i;                  //每個小村從0到N-1編號
		node[i].firstarc = NULL;         //每一個村莊的第一個鄰接邊的指針初始化爲NULL 
	}
	//接下來有N-1行輸入,每行包含一條雙向的兩個端點小村的編號,中間用空格分開。
	//cout << "請輸入村子的" << n - 1 << "條路徑(兩個端點中間用空格分隔):" << endl;
	for (int i = 1; i <= edgenum; i++) {
		cin >> u >> v;
		p = new Edge;               //下面完成鄰接表的建立
		p->adjnode = v;
		p->next = node[u].firstarc;
		node[u].firstarc = p;       //類似於頭插法
		p = new Edge;
		p->adjnode = u;
		p->next = node[v].firstarc;
		node[v].firstarc = p;       //路徑是雙向的
	}
}
void BFS(Node* &node, int root) {    //bfs廣度優先遍歷,預處理出每一個結點的深度和和對應的第2^i個父親結點 
	queue<int>que;                  //隊列
	node[root].depth = 0;           //樹的根結點的深度爲0
	node[root].fa[0] = root;        //樹的根結點的第2^0(第一)個父親結點爲他自己
	que.push(root);                 //根結點入隊 
	while (!que.empty()) {
		int u = que.front();        //將隊列的頭結點u出隊
		que.pop();
		for (int i = 1; i < DEG; i++)    //u的第2^i個父親結點等於u的第2^(i-1)個父親結點的第2^(i-1)個父親結點
			node[u].fa[i] = node[node[u].fa[i - 1]].fa[i - 1];
		Edge* p;
		for (p = node[u].firstarc; p != NULL; p = p->next) {    //找出和u相連的邊
			int v = p->adjnode;                                 //v爲對應鄰接邊的鄰接結點 
			if (v == node[u].fa[0]) continue;                   //因爲存儲的是雙向邊,所以防止再訪問到已經訪問過的父親結點 
			node[v].depth = node[u].depth + 1;                  //結點的深度爲父親結點的深度+1
			node[v].fa[0] = u;                                  //記錄v結點的父親結點爲u
			que.push(v);                                        //v結點入隊
		}
	}
}
int LCA(Node* node, int u, int v) {                       //在樹node中,找出結點u和結點v的最近公共祖先
	if (node[u].depth > node[v].depth)swap(u, v);        //u爲深度較小的結點,v爲深度較大的結點
	int hu = node[u].depth, hv = node[v].depth;
	int tu = u, tv = v;
	for (int det = hv - hu, i = 0; det; det >>= 1, i++)   //兩個結點的深度差爲det,結點v先往上跑det個長度,使得這兩個結點在同一深度 
	if (det & 1)                                      //將深度差拆分成二進制進行結點的2^i跳躍,優化了之前的一個一個跳躍的方法 
		tv = node[tv].fa[i];
	if (tu == tv)return tu;                                //如果他們在同一深度的時候,在同一結點了,那麼這個結點就是這兩個結點的最近公共祖先 
	for (int i = DEG - 1; i >= 0; i--) {                    //他們不在同一結點,卻在同一深度了,那就兩個結點一起往上跳2^i單位
		if (node[tu].fa[i] == node[tv].fa[i])                 //如果會跳過頭了,則跳過這一步跳躍 
			continue;
		tu = node[tu].fa[i];
		tv = node[tv].fa[i];
	}
	return node[tu].fa[0];                                  //循環結束後,這兩個結點在同一深度,並且差一個單位就會跳躍到同一個結點上,
}                                                           //則它們結點的第一個父親結點就是它們的最近公共祖先 
void solve(Node* node, int a, int b, int c) {                       //在樹node中,查詢結點c是否在a和b的路徑上 
	int d = LCA(node, a, b);                                         //找出a和b結點的最近公共祖先爲d 
	int ac = LCA(node, a, c);                                        //找出a和c結點的最近公共祖先爲ac 
	int bc = LCA(node, b, c);                                        //找出b和c結點的最近公共祖先爲bc
	//cout<<d<<" "<<ac<<" "<<bc<<endl;
	if (ac == c&&bc == c) {                                           //如果ac==c並且bc==c,說明c結點是a和b結點的公共祖先 
		if (c == d) {                                               //如果c==d,說明c就是a和b的最近公共祖先,c必定在a和b的路徑上 
			cout << "Yes" << endl;
		}
		else
			cout << "No" << endl;                                    //如果c!=d,說明c不是a和b的最近公共祖先,a和b的路徑上不包括c 
	}
	else if (ac == c || bc == c) {                                    //c是a的祖先或者是b的祖先,說明c在a到d的路徑上或者在b到d的路徑上 
		cout << "Yes" << endl;                                       //此時c一定是a和b路徑上的點 
	}
	else {
		cout << "No" << endl;                                        //如果c不是a的祖先,也不是b的祖先,則a和b的路徑上不會經過c點 
	}
}
int main() {
#ifndef OnLINE_JUGE
	freopen("D:\\test_in0.txt", "r", stdin);                        //數據輸入爲測試數據文件test_in.txt的內容
	freopen("D:\\test_out0_2.txt", "w", stdout);                    //數據輸出到結果文件test_out_2.txt中 
#endif 
	int T, n, m;                              //T爲測試樣例個數,每一組測試樣例包括n個村子,n-1個村子之間的關係,m組詢問            
	int a, b, c;                              //每組詢問包括三個結點編號a,b和c,意思爲詢問c結點是否在a和b結點的路徑上 
	cin >> T;                                 //輸入測試樣例數T 
	while (T--) {
		cin >> n;                             //輸入結點數n 
		Node *node = new Node[n + 1];         //動態申請n+1個結點空間 
		creatgraph(node, n);                 //建樹 
		BFS(node, 0);                        //bfs預處理出樹結點的深度和對應的父親結點 
		cin >> m;                             //輸入m個詢問                
		while (m--) {
			cin >> a >> b >> c;                   //輸入結點編號a,b和c 
			solve(node, a, b, c);              //詢問c結點是否在a和b結點的路徑上
		}
		delete node;                        //這組測試樣例處理結束,撤銷申請的空間 
	}
	return 0;
}

2.測試固定查詢數,測試結點數對程序運行的影響:

0.測試文件test_in1_0.txt  (結點數:10)

運行10個結點,500個查詢的測試樣例平均時間爲0.008538s(上面的程序測試的是10組數據,下面的測試也是一樣)

1. 測試文件test_in1_1.txt   (結點數:20)

運行20個結點,500個查詢的測試樣例平均時間爲0.009514s

2.測試文件test_in1_2.txt   ( 結點數:50)

運行50個結點,500個查詢的測試樣例平均時間爲0.01134s

3.測試文件test_in1_3.txt   (結點數:100)

運行100個結點,500個查詢的測試樣例平均時間爲0.0131s

4. 測試文件test_in1_4.txt  (結點數:500)

運行500個結點,500個查詢的測試樣例平均時間爲0.01524s

5.測試文件test_in1_5.txt  (結點數:1000)

運行1000個結點,500個查詢的測試樣例平均時間爲0.01829s

6.測試文件test_in1_6.txt  (結點數:2000)

運行2000個結點,500個查詢的測試樣例平均時間爲0.02163s

7. 測試文件test_in1_7.txt  (結點數:5000)

運行5000個結點,500個查詢的測試樣例平均時間爲0.03852s

8. 測試文件test_in1_8.txt  (結點數:10000)

運行10000個結點,500個查詢的測試樣例平均時間爲0.05924s

9.測試文件test_in1_9.txt  (結點數:50000)

 

運行50000個結點,500個查詢的測試樣例平均時間爲0.2699s

在500個詢問下,在10~2000個結點組成的樹,時間複雜度基本符合O(Qln(N)),其中,Q爲詢問的次數,在這次驗證中Q爲常數500,N爲樹結點的個數。但是在5000,10000,50000個點的時候,就不在符合這條曲線了,有點接近O(QN)的,思考了一下,覺得是輸入和輸出的數據量過大,在不斷的讀寫文件的過程中,消耗的時間與程序算法求出結果的時間相比起來,在讀寫文件消耗的時間更起決定性作用,所以在後續的運行中,程序運行的時間與讀寫數據的容量大小成線性關係。(如下圖所示)

3.測試固定結點數爲50000,查詢數對程序運行的影響:

0. 測試文件test_in2_0.txt  (查詢數:10)

運行50000個結點,10個查詢的測試樣例平均時間爲0.2539s(上面的程序測試的是10組數據,下面的測試也是一樣)

1.測試文件test_in2_1.txt   (查詢數:50)

運行50000個結點,50個查詢的測試樣例平均時間爲0.2553s

2.測試文件test_in2_2.txt   (查詢數:100)

運行50000個結點,100個查詢的測試樣例平均時間爲0.2598s

3.測試文件test_in2_3.txt   (查詢數:500)

運行50000個結點,500個查詢的測試樣例平均時間爲0.2685s

4.測試文件test_in2_4.txt  (查詢數:1000)

運行50000個結點,1000個查詢的測試樣例平均時間爲0.286s

5.測試文件test_in2_5.txt  (查詢數:5000)

運行50000個結點,5000個查詢的測試樣例平均時間爲0.3113s

6.測試文件test_in2_6.txt  (查詢數:10000)

運行50000個結點,10000個查詢的測試樣例平均時間爲0.365s

7.測試文件test_in2_7.txt  (查詢數:50000)

運行50000個結點,50000個查詢的測試樣例平均時間爲0.7855s

8.測試文件test_in2_8.txt  (查詢數:100000)

運行50000個結點,100000個查詢的測試樣例平均時間爲1.306s

9.測試文件test_in2_9.txt  (查詢數:250000)

運行50000個結點,250000個查詢的測試樣例平均時間爲2.977s

在50000個結點組成的樹,詢問次數爲10~250000次的時間複雜度基本符合O(Qln(N)),其中,Q爲詢問的次數,N爲樹結點的個數,在這次驗證中,N爲常數50000。雖然符合推出來的時間複雜度,但是後面的數據也有可能是收到了大量查詢數據的讀入和寫出的的影響,從而趨近於和查詢次數Q成線性關係的。

另外,影響程序運行速度的,除了程序的算法外,還有電腦的並行運行,其他的軟件在後臺運行佔用CPU,影響該程序的測試時間,其他還有在讀取txt的內容和寫入txt內容的消耗時間等等的其他因素的影響。

4.用普通的廣度優先方法和這次實驗優化後的程序跑這一組特殊的測試樣例

測試樣例 test_in0(10組測試樣例,每一組測試樣例有50000,個結點,10000個詢問,生成的樹退化成單鏈表)

未優化的廣度優先在處理變成退化成單鏈表的時候,運行效率大幅降低

運行所花的時間爲56.48s

本次實驗中寫到的優化程序在處理變成退化成單鏈表的時候,運行效率還是非常優秀,運行所花的時間爲3.692s

.存在的問題及體會

在本次實驗報告中,選擇了第三個課題“神祕國度的愛情故事”。

在一開始選擇實現的算法的時候,我主要考慮了能夠高效和快速的解決問題的算法LCA+ST的算法,這個算法可以在預處理一遍N個結點後,很穩定的在O(ln(N))的時間複雜度找出任意兩個結點之間的最近公共祖先,然後通過AB,AC,BC之間的最近公共祖先的關係可以判斷C是否在A和B結點的路徑上的問題。通過和老師的溝通和交流,我也明白了,這個作爲課程設計的題目,不僅僅是解決課設中的問題,還要考慮到各種優化的策略,程序的時間和空間複雜度,程序的可讀性,程序的魯棒性,程序運行時的真實時間,它隨問題規模的增大會有什麼變化。我原本的做法是基於dfs深度優先搜索的,在深度優先的過程中,50000個結點還是可以存儲在堆棧中的,但是500000個結點的話,程序就會提示棧溢出的問題了,所以我原本的LCA+ST的做法在處理更大的問題規模的時候可能就會出錯了。另外,老師提到的在bfs遍歷的時候可以之間入隊和出隊操作,比dfs的回溯更能節省時間和空間,因此,在和老師的指導下,我選擇了廣度優先生成樹來處理本次課設的問題。

在用廣度優先遍歷生成一棵樹後,可以很容易的就想到的一種做法是,A結點和B結點一個一個的不斷往它們對應的父親結點跳躍,當它們之間有一個結點跳躍到C結點的時候,說明C點在A和B結點之間,此時成功找到答案,結束這次查詢。如果它們先跳到它們已經跳躍過的結點上的話(即B可能跳到A已經跳過的結點上,或者A跳到已經跳過的結點上),那麼該結點就是它們的最近公共祖先,此時還沒找到C結點,則說明C結點不在它們的路徑上。這樣問題也能的到完美的解決。我們用隨機生成的樹和詢問來測試這個程序,可以發現,在生成的樹在完全隨機的情況下,樹都是長的非常均勻的,這樣,這個算法在一次詢問中找到答案的平均時間複雜度爲O(k),其中k爲樹的層數。這個算法非常簡單易懂,而且效率也非常優秀,但是卻有一個嚴重的缺陷,就是在處理極端情況下(比如說一顆樹退化成一個鏈表的時候),樹的層數會變的非常大,導致結點在一個個跳躍的時候,會浪費大量的時間,導致程序時間複雜度退化到O(N),N爲結點的個數。

結點一個一個往上跳躍會浪費大量的時間,那麼我們可以優化一下跳躍的方式。這裏我使用了二進制的跳躍方式,每一次跳躍都是2^i個單位。我把查詢C在A和B的路徑上步驟分成兩部分進行。第一部分是解決給定兩個結點u和v,求出u和v的最近公共祖先(lca),這一步的時間複雜度爲O(ln(n)),第二部分就是求出A和B,A和C,B和C的最近公共祖先,通過對比它們之間的關係,即可得知C點是否在A和B點的路徑上(這部分在以上的實驗內容的思路分析上有畫圖講解和簡單證明),這樣子處理數據的話,就可以在極端數據的保持程序的高效性,在實驗驗證的第三個驗證可以看出優化和沒有優化的程序在處理極端數據之間的區別。

以上便是我在本次課設遇到的問題,以及解決問題思路的變化過程。通過這次課程設計實驗,我對樹的廣度優先遍歷,樹的鄰接表存儲方式有了更深刻的瞭解。通過對課設題目的分析,想出算法,寫出程序,再進一步優化,來解決問題,要考慮很多的問題,比如說,程序的時間和空間複雜度,程序的可讀性,程序的魯棒性,程序運行時的真實時間,它隨問題規模之間的關係,優化的策略,優化後的真正效果是怎麼樣的。尤其是在自己編寫一個隨機生成隨機樹和隨機查詢數據的時候,這都是對自己能力的一個巨大考驗。對於一個隨機生成的合法的數據,通過對拍優化和未優化的程序生成的結果,可以得出程序正確的結果。對於不同規模的數據問題,我們可以對比它們在優化程序下的運行速度。在這裏有一個一直困擾我的問題是,在樹結點的問題規模在5000內情況下,程序的時間複雜度基本爲O(Qln(N)),其中,Q爲詢問的次數,N爲結點的個數。但是在結點數不斷的增大的時候,它們之間的關係不在是對數關係,而是趨近於線性關係。我思考了不少可能情況,最有可能的是,在樹結點太多的時候,程序讀寫txt中大量的測試數據所消耗的時間佔程序運行所畫的時間的比例在不斷的增大,最終讀取txt文件的時間更占主導地位。我嘗試只處理問題,而不輸出結果的方式跑一邊50000個結點的數據,程序運行時間確實少了一些,當時,需要處理的輸入數據還是很大,而且沒有輸出的程序也沒有任何意義,所以這裏就沒有比較好的解決方法,可以只測試程序解決問題的時間,而不考慮程序的讀寫txt文件的時間。另外,需要注意一下的是,程序運行的時候要讀取txt文件的話,需要注意文件下的路徑需要填寫正確。最後,這份課設能夠使用優化的策略進行各個方面的分析,解決問題,還要感謝老師對我的耐心指導。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章