中國大學MOOC-陳越、何欽銘-數據結構-2019夏

----------------------函數題 (共 65 分) ----------------------------

01-複雜度1 最大子列和問題

算法1:在線算法。

#include <cstdio>
int main()
{
    int K, MaxSum = 0, ThisSum = 0, number;
    scanf("%d", &K);
    for (int i = 0; i < K; i++) {
    	scanf("%d", &number);
        ThisSum += number;
        if (ThisSum > MaxSum) MaxSum = ThisSum;
        if (ThisSum < 0) ThisSum = 0;
    }
    printf("%d", MaxSum);
}

算法2:分治法,O(nlogn)

#include <cstdio>
int a[100100];
int Max3( int A, int B, int C )
{   /* 返回3個整數中的最大值 */
    return A > B ? A > C ? A : C : B > C ? B : C;
}
int MaxSubSum(int A[], int left, int right) 
{   /* 分治法求List[left]到List[right]的最大子列和 */
    int MaxLeftSum, MaxRightSum, mid = (left + right) / 2; /* 存放左右子問題的解 */
    /* Base Case 將負數處理爲0*/ 
    if (left == right) {  /* 遞歸的終止條件,子列只有1個數字 */
        return  A[left] > 0 ? A[left] : 0;
    }
    /* DIVIDE PART */
    /* Recursive Case 遞歸地分成兩個子問題求解, 求得兩邊子列的最大和 */  
    MaxLeftSum = MaxSubSum(A, left, mid);
    MaxRightSum = MaxSubSum(A, mid + 1, right);

    /* CONQUEUR PART 下面求跨分界線的最大子列和 */  
    int MaxLeftBorderSum, LeftBorderSum, MaxRightBorderSum, RightBorderSum; /*存放跨分界線的結果*/
    /* 求出前半部分的最大和 */
    MaxLeftBorderSum = LeftBorderSum = 0;
    for (int i = mid; i >= left; i--) {    /* 從中線向左掃描 */
        LeftBorderSum += a[i];
        if (LeftBorderSum > MaxLeftBorderSum) MaxLeftBorderSum = LeftBorderSum;
    } /* 左邊掃描結束 */
    /* 求出後半部分的最大和 */
    MaxRightBorderSum = RightBorderSum = 0; 
    for (int i = mid+1; i <= right; i++) { /* 從中線向右掃描 */
        RightBorderSum += a[i];
        if (RightBorderSum > MaxRightBorderSum) MaxRightBorderSum = RightBorderSum;    
    } /* 右邊掃描結束 */
    /* 下面返回"治"的結果 */
    return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum);
}
int MaxSubsequence(int A[], int k) { /* 保持與前2種算法相同的函數接口 */
    return MaxSubSum(A, 0, k-1);
}
int main()
{
    int k;
    scanf("%d", &k);
    for (int i = 0; i < k; i++) {
        scanf("%d", &a[i]);
    }
    printf("%d", MaxSubsequence(a, k));
}

02-線性結構1 兩個有序鏈表序列的合併

這裏的關鍵在於對於帶頭結點的鏈表的操作,其爲空表,需要將頭結點的指針域置爲NULL。
這裏不知道爲什麼不可以比較p指針和l1指針,判斷它們是否都指向鏈表第一個數據結點,所以只好使用flag。

List Merge( List L1, List L2 ) {
    /*L1 L2均指向無實際數據的頭結點 */ //List l1 = L1->Next, l2 = L2->Next;
    if (!L1->Next || !L2->Next) return L1 ? L1 : L2;
    List Head = (List)malloc(sizeof(struct Node)), This, p = L1->Next, q = L2->Next;
    L1->Next = L2->Next = NULL; /* 使其爲空,需要將頭結點的指針域置爲NULL */
    int flag = 0;
    while (p && q) {
        if (p->Data <= q->Data) {
            if (!flag) { /* 不可以比較p和l1 = L1->Next, 迴避指針的直接比較*/
            	Head->Next = p;
            	flag = 1;
			}
            else This->Next = p;
            This = p;
            p = p->Next;
            if (!p) This->Next = q;
            
        } else {
            if (!flag) {
            	Head->Next = q;
            	flag = 1;
			}
            else This->Next = q;
            This = q;
            q = q->Next;
            if (!q) This->Next = p;
        }
    }
    return Head;
}

04-樹7 二叉搜索樹的操作集 (30 分)

BinTree Insert(BinTree BST, ElementType X) {
	/* Find操作的改版 在找不到的時候進行操作; 函數Insert將X插入二叉搜索樹BST並返回結果樹的根結點指針 */
	if (BST == NULL) {
        BST = (BinTree)malloc(sizeof(struct TNode));
		BST->Data = X;
		BST->Left = BST->Right = NULL;
	} else {
		if (X < BST->Data) BST->Left = Insert(BST->Left, X);
		else if (X > BST->Data) BST->Right = Insert(BST->Right, X);
		/* else do nothing */
	}
	return BST; /* Don't forget this line! */
}
BinTree Delete(BinTree BST, ElementType X) {
	/* Find操作的改版 在找到後進行判斷和操作; 
	函數Delete將X從二叉搜索樹BST中刪除,並返回結果樹的根結點指針;如果X不在樹中,
	則打印一行Not Found並返回原樹的根結點指針 */
	if (BST == NULL) {
		printf("Not Found\n");
	} else {
		if (X < BST->Data) BST->Left = Delete(BST->Left, X);
		else if (X > BST->Data) BST->Right = Delete(BST->Right, X);
		else { 
		    Position TmpCell;
            if (BST->Left && BST->Right) { /*BST has both two child */
            	TmpCell = FindMin(BST->Right);
            	BST->Data = TmpCell->Data; /* 找到右子樹的最小值放在根上面, 它大於左子樹 */
            	/* 遞歸刪除右子樹最小元, 它沒有左子樹 */ 
            	BST->Right = Delete(BST->Right, TmpCell->Data); 
			} else { /* zero childTree or one child, left or right */
            	TmpCell = BST; /* Don't forget! */
            	if (BST->Left) {         /* 左子樹非空 */
            		BST = BST->Left; 
				} else if (BST->Right) { /* 右子樹非空 */
					BST = BST->Right;
				} else {                 /* 爲葉子結點 */
					BST = NULL;
				} 
            	free(TmpCell);
			}
		}
	}
	return BST;
}
Position Find(BinTree BST, ElementType X) {  /* 遞歸版本 */
	/* 函數Find在二叉搜索樹BST中找到X,返回該結點的指針;如果找不到則返回空指針 */
	if (BST == NULL) return NULL;
	else {
		if (X < BST->Data) return Find(BST->Left, X);
		else if (X > BST->Data) return Find(BST->Right, X);
		else return BST;
	}
}
Position FindMin(BinTree BST) {  /* 遞歸版本 */ 
    /* 函數FindMin返回二叉搜索樹BST中最小元結點的指針 */
	if (BST == NULL) return NULL;
	else {
		if (BST->Left == NULL) return BST;
		else return FindMin(BST->Left);
	}
} 
Position FindMax(BinTree BST) {  /* 遞歸版本 */
    /* 函數FindMax返回二叉搜索樹BST中最大元結點的指針 */
	if (BST == NULL) return NULL;
	else {
		if (BST->Right == NULL) return BST;
		else return FindMax(BST->Right);
	}
} 

----------------------編程題 (共 845 分)---------------------------

01-複雜度2 Maximum Subsequence Sum

這道題是最大子列問題的變體,不同的是要求輸出最大子列的第一位數和最後一位數,而且這道題還多加了一些限制

  • If all the K numbers are negative, then its maximum sum is defined to be 0, and you are supposed to output the first and the last numbers of the whole sequence.數組全是負數時,要按“0 數組第一位數 數組最後一位數”輸出。 如-5 -2 -5要按0 -5 -5輸出;
  • The maximum subsequence is not unique, output the one with the smallest indices i and j. 比如:並列和對應相同i但是不同j,即尾是0,輸出(MaxSum, i, min(j1, j2,...);最大和前面有一段是0,輸出(MaxSum, 0, j)
  • 數組不全爲負數時,又分爲:
    • 沒有正數,即負數和0,如-5 0 -6要按0 0 0輸出
    • 有正數,有正數時要考慮一些特殊情況。如:-11 0 2 5這是輸出的最大子列和的第一位數和最後一位數是0 5.
    • ③當結果不唯一時,要求輸出結果對應的具有最小的i和j。即當兩個並列和對應相同i但是不同j,即尾是0,這時考慮的哪個元素個數少就取哪個。

可以說看懂了這兩句話纔看懂了這個題目。

#include <cstdio>
int a[10010];
int main()
{
    int K;
    scanf("%d", &K);
    int ThisSum, ThisLeft = 0;  // 指示當前子列和的左位序, 如果當前子列和不小於0不會變化  
    int MaxSum = -1, MaxLeft = 0, MaxRight = K-1; // 指示最大子列和的左右位序,需要更新的時候就分別更新
    for (int i = 0; i < K; i++) {
        scanf("%d", &a[i]);    
        ThisSum += a[i];
        if (ThisSum > MaxSum) {
            MaxSum = ThisSum;
            MaxLeft = ThisLeft; 
            MaxRight = i;     
        } else if (ThisSum < 0) { // 這裏一定要用else if??
            ThisSum = 0;
            ThisLeft = i + 1;
        }
    }
    if (MaxSum < 0) 
        printf("0 %d %d", a[MaxLeft], a[MaxRight]);  
    else 
        printf("%d %d %d", MaxSum, a[MaxLeft], a[MaxRight]);        
    return 0;
}

My Problem
這題我有很大的疑惑,爲什麼寫else if就全部正確,而寫else就全部錯誤呢?在我的電腦裏面兩者測試了好幾組數據都是相同的結果。
在這裏插入圖片描述在這裏插入圖片描述

02-線性結構2 一元多項式的乘法與加法運算 (20 分)

#include <cstdio>
#include <cstdlib>
/* 一元多項式結構表示 */
typedef struct pNode *PtrToNode;
struct pNode {
	int coef;
	int expo;
	PtrToNode next;	
}; 
typedef PtrToNode Polynomial;
int IsEmpty(Polynomial p) {
	return p->next == NULL;
}
Polynomial AttachOne(int coef, int expo, Polynomial *pRear) {
	Polynomial p = (Polynomial)malloc(sizeof(struct pNode));
	p->coef = coef;
	p->expo = expo;
	p->next = NULL;
	/* 將p指向的新結點插入到結果表達式尾項的後面 */
	(*pRear)->next = p;
	*pRear = p;  /*修改pRear值 */ 
}
/* 創建帶頭鏈表 */ 
Polynomial CreatePoly() {
    Polynomial head = (Polynomial)malloc(sizeof(pNode));
	if (head == NULL) printf("Fatal Error!");
	else head->next = NULL;
	return head;	
}
/* 讀入數據創建並初始化一元多項式 */ 
Polynomial InitPolynomial() {
	int N, coef, expo;
	scanf("%d", &N);
	Polynomial head = CreatePoly(), rear = head;  /* head爲空的表頭結點 */
	while (N--) {
		scanf("%d %d", &coef, &expo);	  // 以係數指數對形式輸入 
		AttachOne(coef, expo, &rear); 
	}
	return head;
}

/* 將未處理完的另一個多項式的所有節點依次複製到結果多項式中去 */
void AttachAll(Polynomial head, Polynomial other) {
	while (other) {
		Polynomial item = (Polynomial)malloc(sizeof(pNode));
		item->coef = other->coef;
		item->expo = other->expo;
		item->next = NULL;
		/* 移動到下一個結點 */
		head->next = item;
		head = item;
		other = other->next;
	}
}
int Compare(int n, int m) {
	if (n > m) return 1;
	else if (n == m) return 0;
	else return -1;
}
Polynomial AddPoly(Polynomial head1, Polynomial head2) {
	/*算法思路: 兩個指針P1和P2分別指向這兩個多項式第一個結點,不斷循環:
    1. P->expo > Q->expo: 將P的當前項存入結果多項式,並使P指向下一項;
	2. P->expo < Q->expo: 將Q的當前項存入結果多項式,並使Q指向下一項;
	3. P->expo == Q->expo: 係數相加,若結果不爲0,則作爲結果多項式對應項
	的係數。同時,P和Q都分別指向下一項;
	4. 當某一多項式處理完時,將另一個多項式的所有結點依次複製到結果多項式中去.
	*/
	Polynomial p = head1->next, q = head2->next, front = CreatePoly();
	Polynomial r = front;
	while (p && q) {   /* 當兩個多項式都有非零項待處理時 */
		switch(Compare(p->expo, q->expo)) {
			case 1:
				AttachOne(p->coef, p->expo, &r);
				p = p->next;
				break;
			case -1:
				AttachOne(q->coef, q->expo, &r);
				q = q->next;
				break; 
			case 0:   
 		        int k = p->coef + q->coef;
				if (k) AttachOne(k, p->expo, &r);  /* 零多項式自然爲空 */	
	            p = p->next;
				q = q->next;
				break;
		}
	}
	if (p) AttachAll(r, p); 
	else if (q) AttachAll(r, q);
	/* 上面兩行可以這麼寫,可以少一個函數 
	for (; p; p = p->next) Attach(p->coef, p->expo, &rear);
	for (; q; q = q->next) Attach(q->coef, q->expo, &rear);
	rear->next = NULL;*/ 
	return front;
}

Polynomial MultiPoly(Polynomial head1, Polynomial head2) {
	if (IsEmpty(head1)) return head1; /* 多項式不存在即爲零多項式, 結果爲0 */ 
	else if (IsEmpty(head2)) return head2;
	
	Polynomial p = head1->next, q = head2->next;
	/* 實現方法 
	1.將乘法運算轉換爲加法運算, 將P當前項(ci,ei)乘P2多項式, 再加到結果多項式裏--太麻煩了 
    2. 逐項插入: 將P當前項(c1i ,e1i)乘Q當前項(c2i,e2i), 並插入到結果多項式中.
	關鍵是要找到插入位置. 初始結果多項式可由P第一項乘Q獲得 */
	int c, e;
	Polynomial front = CreatePoly(), r = front, temp;
	while (q) { /* 初始結果多項式可由P第一項乘Q獲得 */
		AttachOne(p->coef * q->coef, p->expo + q->expo, &r);
		q = q->next; 
	}
	p = p->next; /* p第一項結束 */
	while (p) {
		q = head2->next; /* q重回第一項 */
		r = front;       /* 結果多項式的r指向表頭結點 */ 
		while (q) {
			c = p->coef * q->coef;
			e = p->expo + q->expo;
			while (r->next && r->next->expo > e)   
	            r = r->next;
			if (r->next && r->next->expo == e) {
			    if (r->next->coef + c) {
			    	r->next->coef += c;
				} else {
					temp = r->next;
					r->next = temp->next; // 0項排除 
					free(temp);
				}		 
			}	else {
				temp = (Polynomial)malloc(sizeof(struct pNode));
				temp->coef = c;
				temp->expo = e;
				temp->next = r->next; /* 必須! */
				r->next = temp;
				r = r->next;
			}
			q = q->next;
		}
		p = p->next;
	}
	return front;
}

void PrintPoly(Polynomial head) {
	Polynomial p = head;
	if (IsEmpty(p)) {
	   printf("0 0\n");
	   return;
    }
	p = p->next;
	while (p->next) {
	    printf("%d %d ", p->coef, p->expo);
		p = p->next;	
	}
	printf("%d %d\n", p->coef, p->expo);
} 

int main()
{
	Polynomial p1 = InitPolynomial();
	Polynomial p2 = InitPolynomial();
    
    Polynomial new1 = MultiPoly(p1, p2);
    Polynomial new2 = AddPoly(p1, p2);
    PrintPoly(new1);
    PrintPoly(new2);
    
	return 0;
}

02-線性結構3 Reversing Linked List (25 分)

應該考慮輸入樣例中有不在鏈表中的結點的情況。所以用個sum計數。而且,algorithm頭文件裏面有reverse函數可以直接調用

最大N,最後剩K-1不反轉	答案正確
有多餘結點不在鏈表上	答案正確
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100005;
//靜態鏈表
struct node {
    int data, next;
} List[maxn];

int main() {
    int first, n, k, temp;
    scanf("%d%d%d", &first, &n, &k);
    while (n--) {
        cin >> temp;
        cin >> List[temp].data >> List[temp].next;
    }
    int sum = 0, link[maxn]; //記錄存在鏈表中的元素個數
    while (first != -1) {
        link[sum++] = first;
        first = List[first].next; //記錄頭指針和鏈接
    }
    for (int i = 0; i < sum - sum % k; i += k)  //剩下的小於k的不反轉
        reverse(link + i, link + i + k);
    for (int i = 0; i < sum - 1; i++)  
        printf("%05d %d %05d\n", link[i], List[link[i]].data, link[i + 1]);
    printf("%05d %d -1", link[sum - 1], List[link[sum - 1]]); //打印最後一個結點
    return 0;
}

02-線性結構4 Pop Sequence (25 分)

如何判斷一個容量爲N的堆棧能否形成依次進入的M個數的一個合法出棧序列?

一個相關的問題是:N個數以一定的順序依次入棧(棧容量夠大),(合法的)出棧順序有多少種?

  • 比如,AB兩個元素,入棧順序爲AB,出棧情況有兩種:
    (1)入A,出A,入B,出B,出棧順序爲AB;
    (2)入A,入B,出B,出A,出棧順序爲BA。
    因此,2個元素時,結果爲2。
    顯然f(1)=1,f(2)=2
  • 我們現在來分析一般情況。一般情況下,我們可以按照“第一個入棧的元素,在出棧序列中的位置”作爲分類手段。
    舉個例子,我們假設入棧元素爲A,B,C,D。我們按照“A在出棧序列中的位置”分類討論:
    (1)當A第一個出棧時,A先進,然後馬上出棧。這種情況下,共有“BCD出棧順序的種類數”種方案。也就是f(n-1)。
    (2)當A第二個出棧時,A先進,B再進,之後B需要馬上出來(這樣才能確保A排第二)。此時共有f(n-2)種方案。
    (3)當A第三個出棧時,A先進,之後只要確保排在A後面兩個的元素比A先出即可。此時共有f(2)*f(1)種方案。f(2)是指“BC入棧出棧順序的種類數”,f(1)是指”D入棧出棧的種類數”。
    ……
    分析到這裏,規律就很顯然了。
    在這裏插入圖片描述
    從第一項開始,分別是第一個入棧元素在第i+1個出棧的情況數。上式中,令f(0)=1 。
    這個實際上是卡特蘭數(Catalan number,又稱卡塔蘭數)。若編程實現,需要維護一個一維數組,時間複雜度爲O(n^2)。(遞歸實現的時間複雜度太高)。卡塔蘭數的通項公式爲h(n)=C(2n,n)-C(2n,n+1)(n=0,1,2,…)。
    直接公式:
    令h(0)=1,h(1)=1,卡特蘭數滿足遞推式:h(n)= h(0)*h(n-1)+h(1)*h(n-2) + … + h(n-1)h(0) (n>=2)
    例如:
    h(2)=h(0)h(1)+h(1)h(0)=11+11=2,
    h(3)=h(0)h(2)+h(1)h(1)+h(2)h(0)=12+11+21=5
    遞推關係的解爲:h(n)=C(2n,n)/(n+1) (n=1,2,3,…)
    遞推關係的另類解爲:h(n)=C(2n,n)-C(2n,n+1)(n=1,2,3,…)

常規分析:

  1. 首先,我們設f(n)代表序列個數爲n的出棧序列種數。同時,我們假定第一個出棧的序數是k。
  2. 第一個出棧的序數k將1-n的序列分成兩個序列:其中一個是1-k-1,序列個數爲k-1;另外一個是k+1~n,序列個數是n-k。
  3. 此時,我們若把k視爲一個確定的序數,那麼根據乘法原理,f(n)的問題就等價於序列個數爲k-1的出棧序列種數乘以序列個數爲n - k的出棧序列種數,即選擇k這個序數的出棧組合爲f(k-1)×f(n-k)。
  4. 而k可以選1到n,所以再根據加法原理,將k取不同值的序列種數相加,得到的總序列種數爲:
    f(n)=f(0)f(n-1)+f(1)f(n-2)+……+f(n-1)f(0)。
  5. 看到此處,再看看卡特蘭數的遞推式,答案不言而喻,即爲: f(n)=h(n)=C(2n,n)/(n+1)=C(2n,n)-C(2n,n+1), (n=1,2,3,……)。最後,令f(0)=1,f(1)=1。
#include <cstdio>
#include <cstdlib>
#define EmptyTOS (-1)
#define Error (0)
 
typedef int ElementType;
typedef struct StackRecord {
	int Capacity;
	int TopOfStack;
	ElementType *Array; 
} StackRecord; 
typedef struct StackRecord *Stack;

Stack CreateStack(int MaxSize);
void MakeEmpty(Stack S);
int IsEmpty(Stack S);
int IsFull(Stack S);
void Push(ElementType X, Stack S);
ElementType Pop(Stack S);
void DisposeStack(Stack S);

Stack CreateStack(int MaxSize) {
	Stack S = (Stack)malloc(sizeof(StackRecord));
	S->Array = (ElementType *)malloc(sizeof(ElementType) * MaxSize);
	S->Capacity = MaxSize;
	MakeEmpty(S); /* 將棧設爲空棧 */ 
	return S;
}
void MakeEmpty(Stack S) {
	S->TopOfStack = EmptyTOS; 
}
int IsEmpty(Stack S) {
	return S->TopOfStack == EmptyTOS;
}
int IsFull(Stack S) {
	return S->TopOfStack == (S->Capacity - 1); 
}
void Push(ElementType X, Stack S) {
	if (IsFull(S)) {
        printf("Full Stack");
		return;
	} else 
        S->Array[++S->TopOfStack] = X;
}
ElementType Top(Stack S) { /* 返回棧頂但不使得棧改變 */
	if (IsEmpty(S)) {
		printf("Empty Stack");
		return 0;
	} else 
        return S->Array[S->TopOfStack]; 
}
ElementType Pop(Stack S) { /* 返回棧頂但使得棧改變 */ 
	if (IsEmpty(S)) {
		printf("Empty Stack");
		return Error;      /* Error是ElementType的特殊值, 標誌錯誤 */
	} else 
        return S->Array[S->TopOfStack--];
}
void DisposeStack(Stack S) {
	if (S != NULL) {
		free(S->Array);
		free(S);
	}
}
/* void PrintStack(Stack S) { // 調試用 
	for (int i = 0; i <= S->TopOfStack; i++) {
		printf("%d ", S->Array[i]);
	} 
	printf("---------------------------------------\n");
} */

int main()
{
	int M, N, orders;
	scanf("%d%d%d", &M, &N, &orders);
    //printf("%d %d %d", M, N, orders); /* 調試 */
    Stack S = CreateStack(M); /* 一次申請棧 */ 
	ElementType sin[N], sout[N];
	/* 設置入棧序列 */
	for (int i = 0; i < N; i++) sin[i] = i + 1; 
	
	while (orders--) {
		/* 設置出棧序列 */
		for (int i = 0; i < N; i++) scanf("%d", &sout[i]);
		/* 關鍵點 two pointers 分別指向棧和出棧序列 */
		int i = 0, j = 0;
		for (i = 0; i < N; i++) {			
			Push(sin[i], S); /* 按入棧序列1,2,...,N入棧 */
			// PrintStack(S);  /* 調試 */
			/* 棧非空且棧頂元素等於出棧序列的元素時 */  
			while (!IsEmpty(S) && Top(S) == sout[j]) {
				Pop(S);  /* 彈出該棧頂元素 */
				j++;     /* 當對應j位出棧序列的元素後檢查下一個元素是否對應 */
			}
			/* 棧滿(沒有彈出對應於出棧序列的元素), 表示此限定長度的棧無法該出棧序列 */
			if (IsFull(S)) break; 
		}
		if (i == j) printf("YES\n"); /* 出棧序列合法 */ 
		else printf("NO\n");         /* 出棧序列不合法 */ 
		MakeEmpty(S);                /* 清空棧 */	
	}
	DisposeStack(S); /* 一次釋放棧空間 */
	return 0;
}

這三道二叉樹的題目訓練建樹(靜態結構體數組/指針)、遍歷樹(先序/中序/後序/層序)的基本功

03-樹1 樹的同構 (25 分)

給定兩棵樹T1和T2。如果T1可以通過若干次左右孩子互換就變成T2,則我們稱兩棵樹是“同構”的。例如圖1給出的兩棵樹就是同構的,因爲我們把其中一棵樹的結點A、B、G的左右孩子互換後,就得到另外一棵樹。而圖2就不是同構的。
圖1
在這裏插入圖片描述

圖2
在這裏插入圖片描述
現給定兩棵樹,請你判斷它們是否是同構的。

  • 輸入格式:
    輸入給出2棵二叉樹樹的信息。對於每棵樹,首先在一行中給出一個非負整數N (≤10),即該樹的結點數(此時假設結點從0到N−1編號);隨後N行,第i行對應編號第i個結點,給出該結點中存儲的1個英文大寫字母、其左孩子結點的編號、右孩子結點的編號。如果孩子結點爲空,則在相應位置上給出“-”。給出的數據間用一個空格分隔。注意:題目保證每個結點中存儲的字母是不同的
  • 輸出格式:
    如果兩棵樹是同構的,輸出“Yes”,否則輸出“No”。
  • 輸入樣例1(對應圖1):
    8
    A 1 2
    B 3 4
    C 5 -
    D - -
    E 6 -
    G 7 -
    F - -
    H - -
    8
    G - 4
    B 7 6
    F - -
    A 5 1
    H - -
    C 0 -
    D - -
    E 2 -
    
  • 輸出樣例1:
    Yes
    
  • 輸入樣例2(對應圖2):
    8
    B 5 7
    F - -
    A 0 3
    C 6 -
    H - -
    D - -
    G 4 -
    E 1 -
    8
    D 6 -
    B 5 -
    E - -
    H - -
    C 0 2
    G - 3
    F - -
    A 1 4
    
  • 輸出樣例2:
    No
    

給出的結點順序不一定從根結點開始。不過也無所謂,這裏使用的是靜態結構體數組,可以像完全二叉樹一樣按根結點左右結點的順序排,也可以順便放,只要找到根結點就可以沿着next下標讀取數據

檢查同構是用遞歸函數,先檢查兩棵樹是否是空樹,再檢查兩棵樹是否有一棵是空樹,不然兩棵樹都非空,則檢查它們的結點值,相同的話,就先遞歸檢查T1左子樹和T2左子樹、T1右子樹和T2右子樹是否同構,不同的話就再遞歸檢查T1左子樹和T2右子樹、T1右子樹和T2左子樹是否同構。這是本題的關鍵點。

#include <bits/stdc++.h>
#define Null -1
const int MaxSize = 10;
typedef int Tree;

typedef struct treeNode {
	char data;
	Tree left, right;
} BTreeNode;
BTreeNode T1[MaxSize], T2[MaxSize];

Tree BuildTree(BTreeNode *T);
bool isMorph(Tree R1, Tree R2) ;
void printTree(BTreeNode *T, Tree R) {
	if (R != Null) {
		printf("%c \n", T[R].data);
		printTree(T, T[R].left);
		printTree(T, T[R].right);
	}
}

Tree BuildTree(BTreeNode *T) { 
	int n, i;  //非鏈表順序存儲
	char cl, cr;
	Tree root;
	scanf("%d\n", &n); //n非負整數 
	if (n) {
        int checked[n]; 
		for (i = 0; i < n; i++) checked[i] = 0;
		for (i = 0; i < n; i++) {
			scanf("%c %c %c\n", &T[i].data, &cl, &cr);
			if (cl != '-') {
				T[i].left = cl - '0';
				checked[T[i].left] = 1; //表示該結點被指向了 
			} else T[i].left = Null; 
			if (cr != '-') {
				T[i].right = cr - '0';
				checked[T[i].right] = 1; 
			} else T[i].right = Null; 
		}
		for (i = 0; i < n; i++) {//沒有被指向的結點就是根結點 
			if (!checked[i]) break;
		}
		root = i;
	} else root = Null;
	return root; //返回根結點的位置 
}

bool isMorph(Tree R1, Tree R2) {
	bool flag;	
	//都空樹
	if (R1 == Null && R2 == Null) flag = true; 
	//某一棵爲空 
	else if (R1 == Null || R2 == Null) flag = false; 
	else {	
		 /* compare the data of root */
		if (T1[R1].data != T2[R2].data) flag = false;
		else {
		/* compare the left and right subtree */
		    if (isMorph(T1[R1].left, T2[R2].left) && 
		        isMorph(T1[R1].right, T2[R2].right))
		   	    flag = true;	
	        else if (isMorph(T1[R1].left, T2[R2].right) && 
	                 isMorph(T1[R1].right, T2[R2].left))
       	        flag = true;
    		else 
  			    flag = false;
		}
	}
    return flag;
} 

int main() {	
	Tree R1 = BuildTree(T1), R2 = BuildTree(T2); //建二叉樹
	if (isMorph(R1, R2)) printf("Yes\n");
	else printf("No\n");
	return 0;
}

03-樹2 List Leaves (25 分)

Given a tree, you are supposed to list all the leaves in the order of top down, and left to right.

  • Input Specification:
    Each input file contains one test case. For each case, the first line gives a positive integer N (≤10) which is the total number of nodes in the tree – and hence the nodes are numbered from 0 to N−1. Then N lines follow, each corresponds to a node, and gives the indices of the left and right children of the node. If the child does not exist, a “-” will be put at the position. Any pair of children are separated by a space.
  • Output Specification:
    For each test case, print in one line all the leaves' indices in the order of top down, and left to right. There must be exactly one space between any adjacent numbers, and no extra space at the end of the line.
  • Sample Input:
    8
    1 -
    - -
    0 -
    2 7
    - -
    - -
    5 -
    4 6
    
  • Sample Output:
    4 1 5
    

與上面的一題有點相似,這裏都採用結構體數組,靜態二叉鏈表。然後按照從左到右從上到下列出葉子結點,就是改一下層序遍歷的代碼。另外,爲了滿足輸出的要求,用vector存儲葉子結點的編號

#include <bits/stdc++.h>
using namespace std; 
#define Null -1
typedef int Tree;
const int maxn = 10;

typedef struct BTreeNode {
    int data;
    Tree left, right; 
} BTreeNode;
BTreeNode T[maxn];

Tree CreateTree(BTreeNode *T) {
	Tree root;
	int n;
	scanf("%d\n", &n);
	if (n) {
		char cl, cr;
		int checked[n]; //表示結點是否有父結點 
		for (int i = 0; i < n; i++) checked[i] = 0;
		for (int i = 0; i < n; i++) {
			T[i].data = i; //樹結點的編號0到N-1 
			scanf("%c %c\n", &cl, &cr);
			if (cl != '-') {
			   T[i].left = cl - '0';
			   checked[T[i].left] = 1; 
			} else T[i].left = Null;
			if (cr != '-') {
			   T[i].right = cr - '0';
			   checked[T[i].right] = 1; 
			} else T[i].right = Null;
		}
		int j = 0;
		while (j < n && checked[j] != 0) j++;
		root = j; //找到根結點 
	} else root = -1;
	return root; 
}

void ListLeaves(Tree root) { //層序遍歷 
	queue<int> q; //隊列裏面存儲樹結點編號 
	q.push(root); //根結點入隊 
	vector<int> vi; //存儲葉子結點編號 
	while (!q.empty()) {
		Tree now = q.front(); //取隊首元素
		q.pop(); 
		if (T[now].left == Null && T[now].right == Null) {
			vi.push_back(T[now].data);  
		} 
		if (T[now].left != Null) q.push(T[now].left); //左子樹非空 
		if (T[now].right != Null) q.push(T[now].right); //右子樹非空 
	}
	for (int i = 0; i < vi.size(); i++) {
		if (i > 0) printf(" ");
		printf("%d", vi[i]);
	}
}

int main() {
	Tree root = CreateTree(T);
	ListLeaves(root);
	return 0;
}

★★★ 03-樹3 Tree Traversals Again (25 分)

An inorder binary tree traversal can be implemented in a non-recursive way with a stack. For example, suppose that when a 6-node binary tree (with the keys numbered from 1 to 6) is traversed, the stack operations are: push(1); push(2); push(3); pop(); pop(); push(4); pop(); pop(); push(5); push(6); pop(); pop(). Then a unique binary tree (shown in Figure 1) can be generated from this sequence of operations. Your task is to give the postorder traversal sequence of this tree.

  • Figure 1
    在這裏插入圖片描述
  • Input Specification:
    Each input file contains one test case. For each case, the first line contains a positive integer N (≤30) which is the total number of nodes in a tree (and hence the nodes are numbered from 1 to N). Then 2N lines follow, each describes a stack operation in the format: “Push X” where X is the index of the node being pushed onto the stack; or “Pop” meaning to pop one node from the stack.
  • Output Specification:
    For each test case, print the postorder traversal sequence of the corresponding tree in one line. A solution is guaranteed to exist. All the numbers must be separated by exactly one space, and there must be no extra space at the end of the line.
  • Sample Input:
    6
    Push 1
    Push 2
    Push 3
    Pop
    Pop
    Push 4
    Pop
    Pop
    Push 5
    Push 6
    Pop
    Pop
    
  • Sample Output:
    3 4 2 6 5 1
    

這道題的基礎在於如何表示和建立一棵樹,關鍵在於瞭解使用棧非遞歸地先序/中序/後序遍歷二叉樹,使用隊列非遞歸層序遍歷二叉樹,然後最重要的也是經常考的,就是從①中序與先序、②中序與後序、③中序與層序序列恢復一棵二叉樹

#include <bits/stdc++.h>
using namespace std;
const int MaxSize = 35;

struct BTreeNode {
    int data;
    struct BTreeNode *left;
	struct BTreeNode *right;
};
BTreeNode *newNode(int v) {
    BTreeNode *Node = new BTreeNode;
    Node->data = v;
    Node->left = Node->right = NULL;
    return Node; //返回新建結點的地址 
}

int n;
int preSqu[35], len1 = 0, inSqu[35], len2 = 0;
void Init() { //從輸入的命令中得到先序和中序序列
    stack<int> s;
    char order[10]; int temp;
    for (int i = 0; i < 2 * n; i++) {
        scanf("%s", order);
        if (order[1] == 'u') { //push數據即先序遍歷訪問的時候
            scanf("%d", &temp);
            preSqu[len1++] = temp;
            s.push(temp);
        } else if (order[1] == 'o') { //pop數據即是中序序列訪問的時候
            temp = s.top(); s.pop();
            inSqu[len2++] = temp;
        }
    }
}

BTreeNode* InorderCreateTree(int preL, int preR, int inL, int inR) {
	if (preL > preR) { //空則直接返回 
		return NULL;
	}
	BTreeNode* root = newNode(preSqu[preL]);
	int k; 
	for (k = inL; k <= inR; k++) { //在中序序列中找到根結點 
		if (inSqu[k] == preSqu[preL]) break;
	} 
	int numLeft = k - inL; //左子樹的結點個數 
	//左子樹的先序區間[preL+1, preL+numLeft], 中序區間[inL, k-1]
	//返回左子樹的根結點地址, 賦值給根結點的左指針 
	root->left = InorderCreateTree(preL+1, preL+numLeft, inL, k-1);
	//右子樹的先序區間[preL+numLeft+1, preR], 中序區間[k+1, inR]
	//返回左子樹的根結點地址, 賦值給根結點的左指針 
	root->right = InorderCreateTree(preL+numLeft+1, preR, k+1, inR);
	
	return root; //返回根結點地址 
}

void Postorder(BTreeNode *root) {
	if (root) {
		Postorder(root->left);
		Postorder(root->right);
		printf(--n ? "%d " : "%d", root->data);
	}	
}

int main() {
    scanf("%d", &n);
    Init();
    BTreeNode *T = InorderCreateTree(0, len1 - 1, 0, len2 - 1);
    Postorder(T);
    return 0;
}

這幾道題與二叉搜索樹、平衡樹、完全二叉樹有關,二叉搜索樹的刪除和平衡樹的代碼寫起來有點複雜,但是必須掌握。

04-樹4 是否同一棵二叉搜索樹 (25 分)

給定一個插入序列就可以唯一確定一棵二叉搜索樹。然而,一棵給定的二叉搜索樹卻可以由多種不同的插入序列得到。例如分別按照序列{2, 1, 3}和{2, 3, 1}插入初始爲空的二叉搜索樹,都得到一樣的結果。於是對於輸入的各種插入序列,你需要判斷它們是否能生成一樣的二叉搜索樹

  • 輸入格式:
    輸入包含若干組測試數據。每組數據的第1行給出兩個正整數N (≤10)和L,分別是每個序列插入元素的個數和需要檢查的序列個數。第2行給出N個以空格分隔的正整數,作爲初始插入序列。最後L行,每行給出N個插入的元素,屬於L個需要檢查的序列。
    簡單起見,我們保證每個插入序列都是1到N的一個排列。當讀到N爲0時,標誌輸入結束,這組數據不要處理。
  • 輸出格式:
    對每一組需要檢查的序列,如果其生成的二叉搜索樹跟對應的初始序列生成的一樣,輸出“Yes”,否則輸出“No”。
  • 輸入樣例:
    4 2
    3 1 4 2
    3 4 1 2
    3 2 4 1
    2 1
    2 1
    1 2
    0
    
  • 輸出樣例:
    Yes
    No
    No
    

這一題有多種方法:①全部序列都建樹的方法,代碼如下。②初始序列建樹,其餘序列不建樹,初始二叉搜索樹與其他序列直接比較;③全都不建樹。而且二叉搜索樹和插入、建立、刪除等方法的實現也有多種版本

簡單起見,我先使用了第一種方法。

/* C語言版本 */
node* newNode(ElemType v) {
	node *Node = (node *)malloc(sizeof(struct node));
	Node->data = v;
	Node->left = Node->right = NULL;
	return Node;
}
/* 只用指針, 有返回值的插入版本 */
BST Insert(BST root, ElementType X) {
	if(root == NULL){
		root = newNode(X);
	} else {
		if(item > root->Data)
			root->Right = Insert(root->Right, item);
		else if(item < root->Data)
			root->Left = Insert(root->Left, item);
	} /* root指向每棵子樹的根結點, 根又會發生變化, 因此在最後需要返回 */
	return root; /* 一定要返回! */
}
BST Create(int n) {
	node *root = NULL; //新建根結點root
	ElemType data;
	for (int i = 0; i < n; i++) {
		scanf("%d", &data); //插入函數不一定要返回值 
		root = Insert(root, data);
	} 
	return root;
}

下面的MakeEmpty清空回收函數可以不寫,這裏只是爲了代碼的完整。

/* C++版本 */
#include <bits/stdc++.h>
typedef int ElemType;
typedef struct node {
	ElemType data;
	node *left, *right;
} *BST; //二叉搜索樹 

node* newNode(ElemType v) {
	node *Node = new node;
	Node->data = v;
	Node->left = Node->right = NULL;
	return Node;
}
/* 指針引用, 無返回值的插入版本 */
void Insert(BST &root, ElemType X) {
	if (root == NULL) { //空樹, 插入位置 
		root = newNode(X);
		return; 
	} else {
		if (X > root->data) Insert(root->right, X);
		else if (X < root->data) Insert(root->left, X);
		/* else X is in the tree, do nothing */ 
	} 
}
BST Create(int n) {
	node *root = NULL; //新建根結點root
	ElemType data;
	for (int i = 0; i < n; i++) {
		scanf("%d", &data); //插入函數不一定要返回值 
		Insert(root, data);
	} 
	return root;
}
BST MakeEmpty(BST root) { //有返回值
	if (root) {
		MakeEmpty(root->left);
		MakeEmpty(root->right);
		free(root); //後序清空 
	} 
	return NULL;  //返回空樹 
}

bool isEqualTree(BST t1, BST t2) {
	bool flag;
	if (t1 == NULL && t2 == NULL) flag = true;
	else if (t1 == NULL || t2 == NULL) flag = false;
	else { //compare the value of roots and subtrees
		if (t1->data == t2->data && isEqualTree(t1->left, t2->left) 
		    && isEqualTree(t1->right, t2->right)) 
		   flag = true;
        else flag = false;
	}   
	return flag;
} 

/* 方法: 每個序列都建樹, 將之分別與標準二叉搜索樹比較 */
int main() {
	int N, L;
	BST templateTree, compareTree;
	while (scanf("%d", &N) != EOF && N != 0) {
		scanf("%d", &L);
		/* build template tree */
		templateTree = Create(N);
		/* build compare tree and print the result */ 
		while (L--) {
			compareTree = Create(N);
			/* do the compare */
			if (isEqualTree(templateTree, compareTree)) 
				printf("Yes\n");
            else printf("No\n");
		} 
		templateTree = MakeEmpty(templateTree);
		compareTree = MakeEmpty(compareTree); //清空 
	}
	return 0;
} 

04-樹5 Root of AVL Tree (25 分)

04-樹6 Complete Binary Search Tree (30 分)

A Binary Search Tree (BST) is recursively defined as a binary tree which has the following properties:

The left subtree of a node contains only nodes with keys less than the node’s key.

The right subtree of a node contains only nodes with keys greater than or equal to the node’s key.

Both the left and right subtrees must also be binary search trees.

A Complete Binary Tree (CBT) is a tree that is completely filled, with the possible exception of the bottom level, which is filled from left to right.

Now given a sequence of distinct non-negative integer keys, a unique BST can be constructed if it is required that the tree must also be a CBT. You are supposed to output the level order traversal sequence of this BST.
Input Specification:

Each input file contains one test case. For each case, the first line contains a positive integer N (≤1000). Then N distinct non-negative integer keys are given in the next line. All the numbers in a line are separated by a space and are no greater than 2000.
Output Specification:

For each test case, print in one line the level order traversal sequence of the corresponding complete binary search tree. All the numbers in a line must be separated by a space, and there must be no extra space at the end of the line.
Sample Input:

10
1 2 3 4 5 6 7 8 9 0

Sample Output:

6 3 8 1 5 7 9 0 2 4

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int n, number[maxn], cbt[maxn], k = 0;
void inorder(int root) {
    if(root > n) {
        return ;
    }
    inorder(root * 2);
    cbt[root] = number[k++];
    inorder(root * 2 + 1);
}
int main() {
    scanf("%d", &n);
    for(int i = 0; i < n; i++) 
        scanf("%d", &number[i]); //以完全二叉樹的數組形式存儲
    sort(number, number + n); //排序, 即爲二叉搜索樹的中序序列
    inorder(1); 
    for(int i = 1; i <= n; i++) {
        printf("%d", cbt[i]);
        if(i < n) {
            printf(" ");
        }
    }
    return 0;
}

這幾道題也是必須掌握的數據結構和算法:堆(堆排序)、並查集。以及哈夫曼編碼。是樹的知識的延展。

05-樹7 堆中的路徑 (25 分)

將一系列給定數字插入一個初始爲空的小頂堆H[]。隨後對任意給定的下標i,打印從H[i]到根結點的路徑。

  • 輸入格式:
    每組測試第1行包含2個正整數N和M(≤1000),分別是插入元素的個數、以及需要打印的路徑條數。下一行給出區間[-10000, 10000]內的N個要被插入一個初始爲空的小頂堆的整數。最後一行給出M個下標
  • 輸出格式:
    對輸入中給出的每個下標i,在一行中輸出從H[i]到根結點的路徑上的數據。數字間以1個空格分隔,行末不得有多餘空格。
  • 輸入樣例:
    5 3
    46 23 26 24 10
    5 4 3
    
  • 輸出樣例:
    24 23 10
    46 23 10
    26 10
    

建堆的方法可以是每讀入一個就插入一個;也可以是傳入一個數組序列,然後從N/2的位置開始下濾調整。如果有堆排序的話,一定要額外實現一個下濾函數。上濾函數一般都寫在Insert函數裏面,不太會被調用到。

從子結點到父結點,如果堆從1開始則是下標i/2;堆從零開始則是(i-1)/2。從父結點到左右結點,堆從1開始則是2*i、2*i+1;從0開始則是2*i+1、2*i+2。需要注意這些細小的區別。

#include <bits/stdc++.h>
const int maxn = 1010;
const int MINH = -10001;
int H[maxn], size;

void Insert(int x) {
    int i; //將元素x插入優先隊列小頂堆 
	for (i = ++size; H[i / 2] > x; i /= 2) 
		H[i] = H[i / 2];
	H[i] = x;
}

void CreateHeap(int N) {
	size = 0;
	int t;
	H[0] = MINH; /* 設置崗哨 */
	for (int i = 0; i < N; i++) {
		scanf("%d", &t);
		Insert(t); 
	}
}
int main() {
    int n, m, k;
    scanf("%d%d", &n, &m);
	CreateHeap(n); //根據輸入序列建堆 
	for (int i = 0; i < m; i++) {
		scanf("%d", &k);
		printf("%d", H[k]); //打印到根的路徑 
		while (k > 1) {
			k /= 2;
			printf(" %d", H[k]); 
		}
		printf("\n");
	}
    return 0;
}

★★ 05-樹8 File Transfer (25 分)

We have a network of computers and a list of bi-directional connections. Each of these connections allows a file transfer from one computer to another. Is it possible to send a file from any computer on the network to any other?

  • Input Specification:
    Each input file contains one test case. For each test case, the first line contains N (2≤N≤10​4​​), the total number of computers in a network. Each computer in the network is then represented by a positive integer between 1 and N. Then in the following lines, the input is given in the format:
    I c1 c2  
    
    where I stands for inputting a connection between c1 and c2; or
    C c1 c2    
    
    where C stands for checking if it is possible to transfer files between c1 and c2; or
    S
    
    where S stands for stopping this case.
  • Output Specification:
    For each C case, print in one line the word “yes” or “no” if it is possible or impossible to transfer files between c1 and c2, respectively. At the end of each case, print in one line “The network is connected.” if there is a path between any pair of computers; or “There are k components.” where k is the number of connected components in this network.
  • Sample Input 1:
    5
    C 3 2
    I 3 2
    C 1 5
    I 4 5
    I 2 4
    C 3 5
    S
    
  • Sample Output 1:
    no
    no
    yes
    There are 2 components.
    
  • Sample Input 2:
    5
    C 3 2
    I 3 2
    C 1 5
    I 4 5
    I 2 4
    C 3 5
    I 1 3
    C 1 5
    S
    
  • Sample Output 2:
    no
    no
    yes
    yes
    The network is connected.
    

這道題必須使用按秩歸併(大小)和路徑壓縮,不然有幾個測試點會運行超時。這是我寫的代碼,和算法筆記上面的還是有一些差別的。

#include <cstdio>
const int maxn = 10010;
typedef int ElemType; /* 默認元素可以用非負整數表示 */
typedef int SetName;  /* 默認用根結點的下標作爲集合名稱 */
typedef ElemType DisjSet[maxn]; //不相交集 
DisjSet conn;
int N;

void Initialize(int N) {
	for (int i = 0; i < N; i++) conn[i] = -1; 
}

SetName Find(ElemType X) { //遞歸查找和路徑壓縮 
	if (conn[X] < 0) {
		return X;
	} else {
	    return conn[X] = Find(conn[X]); //追溯父結點的位置		
	}
}

void SetUnion(SetName root1, SetName root2) { //按秩(大小)歸併 
	if (conn[root1] < conn[root2]) { //1集合規模大於2集合 
		conn[root1] += conn[root2];
		conn[root2] = root1;
	} else { //2集合規模大於1集合 
		conn[root2] += conn[root1];
		conn[root1] = root2;
	}
}

void InputConnection() {
	ElemType s1, s2;
	scanf("%d%d\n", &s1, &s2); //編號從1到N, 因此減1
	SetName r1 = Find(s1 - 1), r2 = Find(s2 - 1);
	if (r1 != r2) {
		SetUnion(r1, r2); 
	}	
}

void CheckConnection() {
	ElemType s1, s2;
	scanf("%d%d\n", &s1, &s2);
	SetName r1 = Find(s1 - 1), r2 = Find(s2 - 1);
	if (r1 == r2) printf("yes\n");
	else printf("no\n");
} 

void CheckNetwork() {
	int i, counter = 0;
	for (int i = 0; i < N; i++) {
		if (conn[i] < 0) counter++;
	}
	if (counter == 1) printf("The network is connected.\n");
	else printf("There are %d components.\n", counter);
}

int main() {
	scanf("%d\n", &N);
	Initialize(N);
	char c;
	do {
	   	scanf("%c", &c);
		switch (c) {
			case 'I': InputConnection(); break;
			case 'C': CheckConnection(); break;
			case 'S': CheckNetwork(); break;
		}
	} while (c != 'S');
	return 0;
} 

05-樹9 Huffman Codes (30 分)

In 1953, David A. Huffman published his paper “A Method for the Construction of Minimum-Redundancy Codes”, and hence printed his name in the history of computer science. As a professor who gives the final exam problem on Huffman codes, I am encountering a big problem: the Huffman codes are NOT unique. For example, given a string “aaaxuaxz”, we can observe that the frequencies of the characters ‘a’, ‘x’, ‘u’ and ‘z’ are 4, 2, 1 and 1, respectively. We may either encode the symbols as {‘a’=0, ‘x’=10, ‘u’=110, ‘z’=111}, or in another way as {‘a’=1, ‘x’=01, ‘u’=001, ‘z’=000}, both compress the string into 14 bits. Another set of code can be given as {‘a’=0, ‘x’=11, ‘u’=100, ‘z’=101}, but {‘a’=0, ‘x’=01, ‘u’=011, ‘z’=001} is NOT correct since “aaaxuaxz” and “aazuaxax” can both be decoded from the code 00001011001001. The students are submitting all kinds of codes, and I need a computer program to help me determine which ones are correct and which ones are not.

  • Input Specification:
    Each input file contains one test case. For each case, the first line gives an integer N (2≤N≤63), then followed by a line that contains all the N distinct characters and their frequencies in the following format:
    c[1] f[1] c[2] f[2] ... c[N] f[N]
    
    where c[i] is a character chosen from {'0' - '9', 'a' - 'z', 'A' - 'Z', '_'}, and f[i] is the frequency of c[i] and is an integer no more than 1000. The next line gives a positive integer M (≤1000), then followed by M student submissions. Each student submission consists of N lines, each in the format:
    c[i] code[i]
    
    where c[i] is the i-th character and code[i] is an non-empty string of no more than 63 '0’s and '1’s.
  • Output Specification:
    For each test case, print in each line either “Yes” if the student’s submission is correct, or “No” if not.
  • Note: The optimal solution is not necessarily generated by Huffman algorithm. Any prefix code with code length being optimal is considered correct.
  • Sample Input:
    7
    A 1 B 1 C 1 D 3 E 3 F 6 G 6
    4
    A 00000
    B 00001
    C 0001
    D 001
    E 01
    F 10
    G 11
    A 01010
    B 01011
    C 0100
    D 011
    E 10
    F 11
    G 00
    A 000
    B 001
    C 010
    D 011
    E 100
    F 101
    G 110
    A 00000
    B 00001
    C 0001
    D 001
    E 00
    F 10
    G 11
    
  • Sample Output:
    Yes
    Yes
    No
    No
    


06-圖1 列出連通集 (25 分)

給定一個有N個頂點和E條邊的無向圖,請用DFS和BFS分別列出其所有的連通集。假設頂點從0到N−1編號。進行搜索時,假設我們總是從編號最小的頂點出發按編號遞增的順序訪問鄰接點

  • 輸入格式:輸入第1行給出2個整數N(0<N≤10)和E,分別是圖的頂點數和邊數。隨後E行,每行給出一條邊的兩個端點。每行中的數字之間用1空格分隔。
  • 輸出格式:
    按照"{ v​1​​ v​2​​ … v​k​​ }"的格式,每行輸出一個連通集先輸出DFS的結果,再輸出BFS的結果
  • 輸入樣例:
    8 6
    0 7
    0 1
    2 0
    4 1
    2 4
    3 5
    
  • 輸出樣例:
    { 0 1 4 2 7 }
    { 3 5 }
    { 6 }
    { 0 1 2 7 4 }
    { 3 5 }
    { 6 }
    

寫鄰接矩陣和BFS都比DFS與鄰接表麻煩一點。總之,對於圖要養成一個固定的存圖形式。比如需要遍歷邊的題我用vector<pair, pair> E存邊,不帶權圖鄰接表用vector<int> E[maxn],帶權圖鄰接表用vector<pair<int, int> E[maxn](pair的first是邊的終點,second是邊權)類似的方式。

#include <cstdio>
#include <queue>
#include <vector> 
using namespace std;
/* 鄰接矩陣版本 */
const int maxv = 50; 
const int inf = 1000;
int nv, ne, G[maxv][maxv];
bool vis[maxv] = {false}, inq[maxv] = {false};

void CreateGraph() {
	int u, v;
	for (int i = 0; i < nv; i++) 
		for (int j = 0; j < nv; j++) 
			G[i][j] = inf;
	for (int i = 0; i < ne; i++) {
		scanf("%d%d", &u, &v); //0到N-1編號 
		G[u][v] = 1; //無向圖 
		G[v][u] = 1; 
	}
}

vector<int> comp;
void DFS(int u) {
	vis[u] = true;
	comp.push_back(u);
	for (int v = 0; v < nv; v++) {
		if (vis[v] == false && G[u][v] != inf) {
			DFS(v);
		}
	} 
}
void DFSTrave() {
	for (int i = 0; i < nv; i++) {
		if (vis[i] == false) {
			comp.clear(); 
			DFS(i);
			printf("{");
			for (int j = 0; j < comp.size(); j++) { 
				printf(" %d", comp[j]);
			} 
			printf(" }\n");
		}
	}
}

void BFS(int u) {
	queue<int> q;
	q.push(u);
	inq[u] = true; //u已經加入隊列
	while (!q.empty()) {
		int u = q.front(); q.pop();
	    comp.push_back(u);
		for (int v = 0; v < nv; v++) { //如果u的鄰接點v未曾加入過隊列 
			if (inq[v] == false && G[u][v] != inf) {
				q.push(v); 
				inq[v] = true; //標記v已經加入過隊列 
			}
		}
	} 
}
void BFSTrave() {
	for (int u = 0; u < nv; u++) {
		if (inq[u] == false) { //u未曾加入過隊列 
			comp.clear(); 
			BFS(u); //遍歷u在的連通塊 
			printf("{");
			for (int j = 0; j < comp.size(); j++) { 
				printf(" %d", comp[j]);
			} 
			printf(" }\n");
		}
	} 
}

int main() {
	scanf("%d%d", &nv, &ne);
	CreateGraph();
	DFSTrave();
	BFSTrave();
	return 0;
}

06-圖3 六度空間 (30 分)

“六度空間”理論又稱作“六度分隔(Six Degrees of Separation)”理論。這個理論可以通俗地闡述爲:“你和任何一個陌生人之間所間隔的人不會超過六個,也就是說,最多通過五個人你就能夠認識任何一個陌生人。”如圖1所示。
在這裏插入圖片描述
“六度空間”理論雖然得到廣泛的認同,並且正在得到越來越多的應用。但是數十年來,試圖驗證這個理論始終是許多社會學家努力追求的目標。然而由於歷史的原因,這樣的研究具有太大的侷限性和困難。隨着當代人的聯絡主要依賴於電話、短信、微信以及因特網上即時通信等工具,能夠體現社交網絡關係的一手數據已經逐漸使得“六度空間”理論的驗證成爲可能。

假如給你一個社交網絡圖,請你對每個節點計算符合“六度空間”理論的結點佔結點總數的百分比

  • 輸入格式:
    輸入第1行給出兩個正整數,分別表示社交網絡圖的結點數N(1<N≤10​3​​,表示人數)、邊數M(≤33×N,表示社交關係數)。隨後的M行對應M條邊,每行給出一對正整數,分別是該條邊直接連通的兩個結點的編號(節點從1到N編號)。
  • 輸出格式:
    對每個結點輸出與該結點距離不超過6的結點數佔結點總數的百分比,精確到小數點後2位。每個結節點輸出一行,格式爲“結點編號:(空格)百分比%”。
  • 輸入樣例:
    10 9
    1 2
    2 3
    3 4
    4 5
    5 6
    6 7
    7 8
    8 9
    9 10
    
  • 輸出樣例:
    1: 70.00%
    2: 80.00%
    3: 90.00%
    4: 100.00%
    5: 100.00%
    6: 100.00%
    7: 100.00%
    8: 90.00%
    9: 80.00%
    10: 70.00%
    

無向圖。以下用鄰接表。BFS遍歷,對每個結點儘可能地訪問其連通塊中不超過六層的結點

#include <bits/stdc++.h>
using namespace std;
const int maxv = 1010;
struct node {
	int v, layer; //頂點編號, 頂點層號 
	node(int _v, int _l): v(_v), layer(_l) { }
};
vector<node> Adj[maxv]; //結點從1到N編號
bool inq[maxv] = {false};
int nums[maxv] = {0}; //nums[i]表示與結點i距離不超過6的結點數, 包括自己 
int n, m;

void BFS(int u) {
    queue<node> q;
    q.push(node(u, 0)); //將初始點入隊
	inq[u] = true; 
	while (!q.empty()) {
		node top = q.front(); q.pop();
		int v = top.v; //隊首頂點的編號 
		if (top.layer <= 6) nums[u]++; 
		else break; //超過六度退出 
		for (int i = 0; i < Adj[v].size(); i++) {
			node next = Adj[v][i]; //從v出發能到達的頂點next
			next.layer = top.layer + 1;
			if (inq[next.v] == false) {
				q.push(next); //將k入隊 
				inq[next.v] = true; //標記v已加入隊列 
			}
		}
	} 
}

void BFSTrave() {
    fill(nums, nums + maxv, 0); 
	for (int i = 1; i <= n; i++) {
		fill(inq, inq + maxv, 0); //每一次將之置0 
		BFS(i); //對每個結點輸出與該結點距離不超過6的結點數佔結點總數的百分比
		printf("%d: %.2lf%%\n", i, nums[i] * 100.0 / n);
	} 
}

int main() {
	int u, v;
	scanf("%d%d", &n, &m);
	for (int i = 0; i < m; i++) {
		scanf("%d%d", &u, &v);
		Adj[u].push_back(node(v, 0));
		Adj[v].push_back(node(u, 0)); //無向圖 
	}
	BFSTrave();
	return 0;
}

考察全源最短路算法。

07-圖4 哈利·波特的考試 (25 分)

哈利·波特要考試了,他需要你的幫助。這門課學的是用魔咒將一種動物變成另一種動物的本事。例如將貓變成老鼠的魔咒是haha,將老鼠變成魚的魔咒是hehe等等。反方向變化的魔咒就是簡單地將原來的魔咒倒過來唸,例如ahah可以將老鼠變成貓。另外,如果想把貓變成魚,可以通過念一個直接魔咒lalala,也可以將貓變老鼠、老鼠變魚的魔咒連起來唸:hahahehe。

現在哈利·波特的手裏有一本教材,裏面列出了所有的變形魔咒和能變的動物。老師允許他自己帶一隻動物去考場,要考察他把這隻動物變成任意一隻指定動物的本事。於是他來問你:帶什麼動物去可以讓最難變的那種動物(即該動物變爲哈利·波特自己帶去的動物所需要的魔咒最長)需要的魔咒最短?例如:如果只有貓、鼠、魚,則顯然哈利·波特應該帶鼠去,因爲鼠變成另外兩種動物都只需要念4個字符;而如果帶貓去,則至少需要念6個字符才能把貓變成魚;同理,帶魚去也不是最好的選擇。

  • 輸入格式:
    輸入說明:輸入第1行給出兩個正整數N (≤100)和M,其中N是考試涉及的動物總數,M是用於直接變形的魔咒條數。爲簡單起見,我們將動物按1~N編號。隨後M行,每行給出了3個正整數,分別是兩種動物的編號、以及它們之間變形需要的魔咒的長度(≤100),數字之間用空格分隔。
  • 輸出格式:
    輸出哈利·波特應該帶去考場的動物的編號、以及最長的變形魔咒的長度,中間以空格分隔。如果只帶1只動物是不可能完成所有變形要求的,則輸出0。如果有若干只動物都可以備選,則輸出編號最小的那隻。
  • 輸入樣例:
    6 11
    3 4 70
    1 2 1
    5 4 50
    2 6 50
    5 6 60
    1 3 70
    4 6 60
    3 6 80
    5 1 100
    2 4 60
    5 2 80
    
  • 輸出樣例:
    4 70
    

這就是對每一個結點,都求出其到另外N-1個結點的最短距離,最短距離中最長的那個,就是哈利·波特自己帶去的動物所需要的魔咒最長,最難變成的那個動物。然後從這些最長距離中得到最短的魔咒長度,如果這個長度爲inf,說明做不到。
由於結點數目只有100,使用floyd算法。

#include <bits/stdc++.h>
using namespace std;
const int maxv = 110, inf = 1000000000;
int G[maxv][maxv];
int n, m;
void floyd() {
	for (int k = 1; k <= n; k++) {
		for (int i = 1; i <= n; i++) {
			if (G[i][k] != inf) {
				for (int j = 1; j <= n; j++) {
					if (G[k][j] != inf && G[i][k] + G[k][j] < G[i][j]) {
						G[i][j] = G[i][k] + G[k][j];
					}
				}
			}
		}
	} 
}

int main() {
	int u, v, w;
	scanf("%d%d", &n, &m);
	fill(G[0], G[0] + maxv * maxv, inf);
	for (int i = 1; i <= n; i++) 
		G[i][i] = 0; //自己到自己爲0, 無負環 
	for (int i = 0; i < m; i++) {
		scanf("%d%d%d", &u, &v, &w);
		G[u][v] = G[v][u] = w;
	}
	floyd();
    int mindist = inf, animal = 0;
    //檢查每一行裏面最大的數字 
	for (int i = 1; i <= n; i++) {
		int max = -1;
		for (int j = 1; j <= n; j++) 
			if (G[i][j] > max) 
				max = G[i][j];
		if (max == inf) {
			printf("0\n");
			return 0;
		}
		if (mindist > max) {
			mindist = max; //找到最長距離最小的動物
			animal = i; 
		}
	} 
	printf("%d %d\n", animal, mindist);
	return 0;
} 

考察Dijkstra最短路算法。這裏沒有負環或者負值圈,但是有第二標尺,除了最短路外還有判斷條件。

07-圖6 旅遊規劃 (25 分)

有了一張自駕旅遊路線圖,你會知道城市間的高速公路長度、以及該公路要收取的過路費。現在需要你寫一個程序,幫助前來諮詢的遊客找一條出發地和目的地之間的最短路徑。如果有若干條路徑都是最短的,那麼需要輸出最便宜的一條路徑

  • 輸入格式: 輸入數據的第1行給出4個正整數N、M、S、D,其中N(2≤N≤500)是城市的個數,順便假設城市的編號爲0~(N−1);M是高速公路的條數;S是出發地的城市編號;D是目的地的城市編號。隨後的M行中,每行給出一條高速公路的信息,分別是:城市1、城市2、高速公路長度、收費額,中間用空格分開,數字均爲整數且不超過500。輸入保證解的存在。
  • 輸出格式: 在一行裏輸出路徑的長度和收費總額,數字間以空格分隔,輸出結尾不能有多餘空格。
  • 輸入樣例:
    4 5 0 3
    0 1 1 20
    1 3 2 30
    0 3 4 10
    0 2 2 20
    2 3 1 20
    
  • 輸出樣例:
    3 40
    
#include <bits/stdc++.h>
using namespace std; 
/* dijkstra + 第二標尺最短路 */
const int inf = 1000000000, maxv = 510;
int n, m, s, D; //城市個數 公路條數 出發地編號 目的地編號
int G[maxv][maxv], cost[maxv][maxv]; //0 N-1編號 
int c[maxv], d[maxv]; //數字均爲整數且不超過500 
bool vis[maxv] = {false}; 

void Dijkstra(int s) {
	fill(d, d + maxv, inf);  //將整個d數組初始化爲inf 
	d[s] = 0; //起點距離自己的距離是0 
	fill(c, c + maxv, inf);  //將整個cost數組初始化化爲inf
	c[s] = 0; //起點到達自己的花費爲0 
	for (int i = 0; i < n; i++) {
		int u = -1, min = inf;
		for (int j = 0; j < n; j++) { //找到未訪問頂點中d[v]最小的 
			if (vis[j] == false && d[j] < min) {
				u = j;
				min = d[j]; 
			}
		}
		if (u == -1) return; //其他的頂點與起點S不連通 
		vis[u] = true;
		for (int v = 0; v < n; v++) {
			if (vis[v] == false && G[u][v] != inf) {
				if (d[u] + G[u][v] < d[v]) { //最短路徑和花費 
					d[v] = d[u] + G[u][v];
					c[v] = c[u] + cost[u][v];
				} else if (d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) {
					c[v] = c[u] + cost[u][v]; //最短距離相同時看能否使c[v]最優 
				} 
			} 
		}
	}
}

int main() {
	fill(G[0], G[0] + maxv * maxv, inf); //初始化G 
	fill(cost[0], cost[0] + maxv * maxv, inf); //初始化cost 
	scanf("%d%d%d%d", &n, &m, &s, &D);
	int c1, c2, p, spend; //城市1、城市2、高速公路長度、收費額
	for (int i = 0; i < m; i++) { //城市1、城市2、高速公路長度、收費額
		scanf("%d%d%d%d", &c1, &c2, &p, &spend);
		G[c1][c2] = G[c2][c1] = p; //記錄公路長度 
		cost[c1][c2] = cost[c2][c1] = spend; //記錄收費額 
	} 
	Dijkstra(s);
    //輸出路徑的長度和收費總額
    printf("%d %d\n", d[D], c[D]);
	return 0;  
} 

考察最小生成樹算法。

08-圖7 公路村村通 (30 分)

現有村落間道路的統計數據表中,列出了有可能建設成標準公路的若干條道路的成本,求使每個村落都有公路連通所需要的最低成本

  • 輸入格式:
    輸入數據包括城鎮數目正整數N(≤1000)和候選道路數目M(≤3N);隨後的M行對應M條道路,每行給出3個正整數,分別是該條道路直接連通的兩個城鎮的編號以及該道路改建的預算成本。爲簡單起見,城鎮從1到N編號。
  • 輸出格式:
    輸出村村通需要的最低成本。如果輸入數據不足以保證暢通,則輸出−1,表示需要建設更多公路。
  • 輸入樣例:
    6 15
    1 2 5
    1 3 3
    1 4 7
    1 5 4
    1 6 2
    2 3 4
    2 4 6
    2 5 2
    2 6 6
    3 4 6
    3 5 1
    3 6 1
    4 5 10
    4 6 8
    5 6 3
    
  • 輸出樣例:
    12
    

該條道路直接連通的兩個城鎮的編號就是結點,該道路改建的預算成本即爲邊權。這裏先使用Kruskal算法(編程更簡單),“最短的邊一定在MST上面”,使用邊集數組排序後依次選擇邊權最小的邊,然後使用並查集判斷兩個端點是否在不同的連通塊中,是否會形成迴路。

#include <bits/stdc++.h> 
/* Kruskal + 並查集 + 邊集數組 + 排序 */ 
using namespace std;
const int maxv = 1010; //1000個最大城鎮 
int n, m; //n個結點和m條邊
struct edge { int u, v, w; } E[3 * maxv]; 
bool cmp(edge a, edge b) {
	return a.w < b.w; //邊權從小到大排序 
}

int father[maxv]; //並查集
int initialize() { //結點映射爲1-N編號 
	for (int i = 1; i <= n; i++) father[i] = -1;
} 
int find(int x) {
	if (father[x] < 0) return x;
	else return father[x] = find(father[x]);
}

void setUnion(int u, int v) {
	if (father[u] < father[v]) {
		father[u] += father[v];
		father[v] = u;
	} else {
		father[v] += father[u];
		father[u] = v;
	}
}

int kruskal() {
	int ans = 0;
	initialize(); //初始化, 開始時每個村莊都是獨立的集 
	sort(E + 1, E + 1 + m, cmp); //排序 
	for (int i = 1; i <= m; i++) {
		int a = find(E[i].u); //邊的前端點屬於哪個集
		int b = find(E[i].v); //邊的後端點屬於哪個集
		if (a == b) continue; //產生了圈, 丟棄這個邊 
		else setUnion(a, b); //如果這兩個點不屬於同一個集, 沒修路
		ans += E[i].w; //修路, 添加權重, 計算MST 
	}
	int comps = 0;
	for (int i = 1; i <= n; i++) {
		if (father[i] < 0) comps++; 
	} 
	if (comps == 1) return ans; //全部點都可以通過候選道路中的生成樹連通
	else return -1; 
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++)  //點的編號從1開始 
		scanf("%d%d%d", &E[i].u, &E[i].v, &E[i].w);
	printf("%d\n", kruskal()); 
	return 0;
}

其實,每一次集合並都又連通了一個點,只要連通了N-1次就可以退出了。如果邊太多,連通的話可以提前退出,而無需遍歷完整個邊集數組。

	else {
	    setUnion(a, b); //如果這兩個點不屬於同一個集, 沒修路
	    ans += E[i].w; //修路, 添加權重, 計算MST 
	    cnt++; 
	    if (cnt >= n - 1) return ans;
	}	
	return -1; 
}

使用prim算法的代碼如下,prim算法的思想是“最近的鄰居一定在MST”中,與Dijkstra的唯一區別在於最短距離是頂點Vi針對起點s還是集合S。而且對於prim來說如果僅僅求最小邊權之和,可以隨意指定一個頂點爲初始點。不過prim對於稠密圖效果比較好。
這下面的兩版代碼分別使用了鄰接矩陣和鄰接表,沒有用優先隊列優化,因此不是作爲模板的最好選擇

#include <bits/stdc++.h> 
/* Kruskal + 鄰接矩陣 + 集合S */ 
using namespace std;

const int inf = 1000000000, maxv = 1010; //1000個最大城鎮 
int n, m, G[maxv][maxv], d[maxv]; //n個結點和m條邊; 頂點與集合S的最短距離 
bool vis[maxv] = {false}; //標記數組, 標記頂點加入了集合S

int prim() { //默認1號爲初始點, 返回MST的邊權和 
	fill(d, d + maxv, inf); //將整個d數組賦值爲inf
	d[1] = 0; //只有1號結點的到集合S的距離爲0
	int ans = 0; //MST的邊權和
	for (int i = 0; i < n; i++) { //循環n次
		int u = -1, min = inf; //u使得d[u]最小, min存放最小的d[u]
		for (int j = 1; j <= n; j++) {
			if (vis[j] == false && d[j] < min) {
				u = j;
				min = d[j];
			}
		} 
		//找不到小於inf的d[u], 剩下的頂點和集合S不相通
		if (u == -1) return -1;
		vis[u] = true;
		ans += d[u]; //將與集合S距離最小的邊加入最小生成樹 
	    for (int v = 1; v <= n; v++) {
	    	//v未訪問, u可達v, 以u爲中介點可以使得v離集合S更近
			if (vis[v] == false && G[u][v] != inf && G[u][v] < d[v]) {
				d[v] = G[u][v]; //將G[u][v]賦值給d[v] 
			} 
		}
	}
	return ans; //返回MST的邊權之和 
} 

int main() {
	scanf("%d%d", &n, &m);
	int u, v, w;
	fill(G[0], G[0] + maxv * maxv, inf); //初始化圖G 
	for (int i = 1; i <= m; i++)  {//點的編號從1開始  
		scanf("%d%d%d", &u, &v, &w);
		G[u][v] = G[v][u] = w; //無向圖 
	}
	printf("%d\n", prim()); 
	return 0;
}

感覺prim鄰接表的代碼還有點麻煩,不過和上面鄰接矩陣的差別就只有那個更新d[v]的for循環

#include <bits/stdc++.h> 
/* Kruskal + 鄰接表 + 集合S */ 
using namespace std;
struct node {
	int v, w; //v爲邊的目標, w爲邊權 
	node(int _v, int _w): v(_v), w(_w) { } 
};
const int inf = 1000000000, maxv = 1010; //1000個最大城鎮 
int n, m, d[maxv]; //n個結點和m條邊; 頂點與集合S的最短距離 
bool vis[maxv] = {false}; //標記數組, 標記頂點加入了集合S
vector<node> Adj[maxv]; //鄰接表 

int prim() { //默認1號爲初始點, 返回MST的邊權和 
	fill(d, d + maxv, inf); //將整個d數組賦值爲inf
	d[1] = 0; //只有1號結點的到集合S的距離爲0, 其餘全部爲inf 
	int ans = 0; //MST的邊權和
	for (int i = 0; i < n; i++) { //循環n次
		int u = -1, min = inf; //u使得d[u]最小, min存放最小的d[u]
		for (int j = 1; j <= n; j++) {
			if (vis[j] == false && d[j] < min) {
				u = j;
				min = d[j];
			}
		} 
		//找不到小於inf的d[u], 剩下的頂點和集合S不相通
		if (u == -1) return -1;
		vis[u] = true;
		ans += d[u]; //將與集合S距離最小的邊加入最小生成樹 
	    for (int i = 0; i < Adj[u].size(); i++) { //u可達v
	    	int v = Adj[u][i].v; //通過鄰接表直接獲得u能達到的頂點v 
	    	//v未訪問, 以u爲中介點可以使得v離集合S更近
			if (vis[v] == false && Adj[u][i].w < d[v]) {
				d[v] = Adj[u][i].w; //將G[u][v]賦值給d[v] 
			} 
		}
	}
	return ans; //返回MST的邊權之和 
} 

int main() {
	scanf("%d%d", &n, &m);
	int u, v, w; 
	for (int i = 1; i <= m; i++)  {//點的編號從1開始  
		scanf("%d%d%d", &u, &v, &w);
		Adj[u].push_back(node(v, w)); //無向圖 
		Adj[v].push_back(node(u, w));  
	}
	printf("%d\n", prim()); 
	return 0;
}


09-排序1 排序 (25 分)

給定N個(長整型範圍內的)整數,要求輸出從小到大排序後的結果。本題旨在測試各種不同的排序算法在各種數據情況下的表現。各組測試數據特點如下:

  • 數據1:只有1個元素;
  • 數據2:11個不相同的整數,測試基本正確性;
  • 數據3:103個隨機整數;
  • 數據4:104個隨機整數;
  • 數據5:105個隨機整數;
  • 數據6:105個順序整數;
  • 數據7:105個逆序整數;
  • 數據8:105個基本有序的整數;
  • 數據9:105個隨機正整數,每個數字不超過1000。
  1. 輸入格式:
    輸入第一行給出正整數N(≤10​5​​),隨後一行給出N個(長整型範圍內的)整數,其間以空格分隔。
  2. 輸出格式:
    在一行中輸出從小到大排序後的結果,數字間以1個空格分隔,行末不得有多餘空格。
  • 輸入樣例:
    11
    4 981 10 -17 0 -20 29 50 8 43 -5
    
  • 輸出樣例:
    -20 -17 -5 0 4 8 10 29 43 50 981
    

說是長整數,其實寫int也可以。這一題裏面,我自己先後實現了簡單選擇排序、冒泡排序、插入排序、希爾排序、堆排序、歸併排序和快速排序。儘管實際中很少會遇到自己寫排序算法的時候,一般都是調用庫函數,但我還是感覺有一點收穫。

#include <cstdio>
#include <algorithm>
using namespace std;

void trace(int a[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d", a[i]);
        if (i < n - 1) printf(" ");
    }
    printf("\n");
}

// void BubbleSort(int a[], int n) {
//     for (int i = n - 1; i > 0; i--) {
//         int flag = 1;
//         for (int j = 0; j < i; j++) {
//             if (a[j] > a[j + 1]) {
//                 swap(a[j], a[j + 1]);
//                 flag = 0;
//             }
//         }
//         if (flag) break;
//     }
// }
void InsertionSort(int a[], int n) {
    for (int i = 1; i < n; i++) {
        int temp = a[i], j;
        for (j = i; j > 0 && a[j - 1] > temp; j--) 
            a[j] = a[j - 1];
        a[j] = temp;
    }
}

int main() {
    int n;
    scanf("%d", &n);
    int a[n]; 
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);

    InsertionSort(a, n);
    trace(a, n);
    return 0;
}

10-排序4 統計工齡 (20 分)

給定公司N名員工的工齡,要求按工齡增序輸出每個工齡段有多少員工。

  • 輸入格式:輸入首先給出正整數N(≤10​5​​),即員工總人數;隨後給出N個整數,即每個員工的工齡,範圍在[0, 50]。
  • 輸出格式:按工齡的遞增順序輸出每個工齡的員工個數,格式爲:“工齡:人數”。每項佔一行。如果人數爲0則不輸出該項。
  • 輸入樣例:
    8
    10 2 0 5 7 2 5 2
    
  • 輸出樣例:
    0:1
    2:3
    5:2
    7:1
    10:1
    

整數哈希,遞增打印。

#include <cstdio>
int workage[60] = {0}; //每個員工的工齡,範圍在[0, 50]

int main() {
    int n, t;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &t);
        workage[t]++;
    }
    for (int i = 0; i <= 50; i++) {
        if (workage[i] != 0) printf("%d:%d\n", i, workage[i]);
    }
    return 0;
}

11-散列1 電話聊天狂人 (25 分)

給定大量手機用戶通話記錄,找出其中通話次數最多的聊天狂人

  • 輸入格式:輸入首先給出正整數N(≤10​5​​),爲通話記錄條數。隨後N行,每行給出一條通話記錄。簡單起見,這裏只列出撥出方和接收方的11位數字構成的手機號碼,其中以空格分隔。
  • 輸出格式:在一行中給出聊天狂人的手機號碼及其通話次數,其間以空格分隔。如果這樣的人不唯一,則輸出狂人中最小的號碼及其通話次數,並且附加給出並列狂人的人數
  • 輸入樣例:
    4
    13005711862 13588625832
    13505711862 13088625832
    13588625832 18087925832
    15005713862 13588625832
    
  • 輸出樣例:
    13588625832 3
    

這裏使用了C++11的標準,auto。不瞭解的可以看看這兩篇文章:
https://blog.csdn.net/qq_42957923/article/details/90107840
https://www.cnblogs.com/KunLunSu/p/7861330.html
同時,map可以自動對第一關鍵字進行排序,因此我們可以很容易的得到最小的號碼。

#include <cstdio>
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    int n; scanf("%d", &n);
    string t1, t2;
    map<string, int> mp;
    for (int i = 0; i < n; i++) {
        cin >> t1 >> t2;
        mp[t1]++;
        mp[t2]++;
    }    
    int max = 0, ans = 0;
    for (auto m : mp) {
        if (m.second > max) {
            t1 = m.first;
            max = m.second;
        } 
    }
    for (auto m : mp) {
        if (m.second == max) ans++;
    }
    if (ans > 1) cout << t1 << " " << max << " " << ans << endl;
    else cout << t1 << " " << max << endl;
    return 0;
}

11-散列3 QQ帳戶的申請與登陸 (25 分)

實現QQ新帳戶申請和老帳戶登陸的簡化版功能。最大挑戰是:據說現在的QQ號碼已經有10位數了。

  • 輸入格式:
    輸入首先給出一個正整數N(≤10​5​​),隨後給出N行指令。每行指令的格式爲:“命令符(空格)QQ號碼(空格)密碼”。其中命令符爲“N”(代表New)時表示要新申請一個QQ號,後面是新帳戶的號碼和密碼;命令符爲“L”(代表Login)時表示是老帳戶登陸,後面是登陸信息。QQ號碼爲一個不超過10位、但大於1000(據說QQ老總的號碼是1001)的整數。密碼爲不小於6位、不超過16位、且不包含空格的字符串
  • 輸出格式: 針對每條指令,給出相應的信息:
    1)若新申請帳戶成功,則輸出“New: OK”;
    2)若新申請的號碼已經存在,則輸出“ERROR: Exist”;
    3)若老帳戶登陸成功,則輸出“Login: OK”;
    4)若老帳戶QQ號碼不存在,則輸出“ERROR: Not Exist”;
    5)若老帳戶密碼錯誤,則輸出“ERROR: Wrong PW”。
  • 輸入樣例:
    5
    L 1234567890 [email protected]
    N 1234567890 [email protected]
    N 1234567890 [email protected]
    L 1234567890 myQQ@qq
    L 1234567890 [email protected]
    
  • 輸出樣例:
    ERROR: Not Exist
    New: OK
    ERROR: Exist
    ERROR: Wrong PW
    Login: OK
    

使用了STL的代碼,map也能過,不過時間長一些:

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

int main() {
	int n; scanf("%d", &n);
	unordered_map<string, int> acc; //賬號 
	unordered_map<string, string> psw; //密碼
	char c;
	string a, p;
	for (int i = 0; i < n; i++) {
		cin >> c >> a >> p;
		switch (c) {
			case 'N':
				if (acc[a] == 0) {
					acc[a] = 1; //新申請帳戶
					psw[a] = p; //存入密碼 
					printf("New: OK\n");
				} else printf("ERROR: Exist\n"); //新申請的號碼已經存在
				break;
			case 'L': 
				if (acc[a] == 0) printf("ERROR: Not Exist\n"); //老帳戶QQ號碼不存在
				else if (psw[a] != p) printf("ERROR: Wrong PW\n"); //老帳戶密碼錯誤
				else printf("Login: OK\n");
				break;
		}
	} 
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章