1. 程式人生 > >遞迴_CH0301_遞迴實現指數型列舉_遞迴演算法正確性證明範例

遞迴_CH0301_遞迴實現指數型列舉_遞迴演算法正確性證明範例

點此開啟題目頁面

    簡而言之本題要求列印集{1, 2,..., n}的所有子集(列印時每個子集中的所有元素位於同一行, 每行中的元素遞增列印, 空集對應空行)

先給出如下AC程式碼, 然後給出其正確性的形式化證明

//CH0301_遞迴實現指數型列舉
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int n;//輸出{1, ... n}的所有子集 
vector<int> choosn;//當前已經選擇的元素集
void solve(int cur){
	if(cur > n){
		for(int i = 0; i < choosn.size(); ++i) cout << choosn[i] << " ";
		cout << endl;
		return;
	}
	solve(cur + 1);
	choosn.push_back(cur), solve(cur + 1), choosn.pop_back();
} 
int main(){
	scanf("%d", &n); solve(1);
	return 0;
}

對於上述演算法正確性的證明:

設集P_{i} = { x | x為對於solve(i)的呼叫, 且x滿足本次solve(i)執行初始時刻choosn[0...choosn.size() - 1]嚴格遞增(如果choosn非空)且對應於{1,...,i - 1}的一個子集 }

    歸納起點: 對於集P_{n}中的所有元素, 直接觀察其程式碼邏輯可得: P_{n}中的所有元素均可打印出當前choosn對應集合和集{n}的所有子集的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行中, 並於執行完畢後還原choosn為初始狀態

    遞推: 假設對於對於集P_{k}

 (2 =< k <= n)中的所有元素, 均可打印出當前choosn對應集合和集{k,...,n}的所有子集的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行, 並於執行完畢後還原choosn為初始狀態. 那麼對於P_{k - 1}中的任意元素, 程式第14行可正確列印當前choosn對應集合和集{k,...,n}的所有子集的並集, 並於執行完畢後還原choosn為初始狀態, 程式的第15行可正確列印當前(choosn對應集合\cup{k - 1})和集{k,...,n}的所有子集的並集, 並於執行完畢後還原choosn為初始狀態, 綜合第14行和第15行的作用可得: 集P_{k - 1}
中的所有元素均可打印出當前choosn對應集合和集{k - 1,...,n}的所有子集的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行中, 並於執行完畢後還原choosn為初始狀態

   綜上述: 對於集P_{1}中的所有元素均可打印出當前choosn對應集合(此時為空集)和集{1,...,n}的所有子集的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行中, 並於執行完畢後還原choosn為初始狀態, 這也正是題目要求的結果, 從而演算法正確性得證.

   下面提出一個擴充套件性問題, 將每個子集中的元素按照遞增排序後(視為一個字串), 如何按照字典序遞增列印{1,...,n}的所有子集?(約定: 空集對應空行且列印在第一行), 下面先給出實現程式碼, 隨後對該演算法正確性證明的關鍵部分給予說明, 具體證明過程不再贅述.

//CH0301_遞迴實現指數型列舉_按字典序遞增列印每個子集
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int n;
vector<int> choosn;
void solve(int cur, bool show){
	if(show){
		for(int i = 0; i < choosn.size(); ++i) cout << choosn[i] << " "; cout << endl;
	}
	if(cur > n) return;	
	choosn.push_back(cur), solve(cur + 1, true), choosn.pop_back();
	solve(cur + 1, false);
} 
int main(){
	scanf("%d", &n), solve(1, true);
	return 0;
}

    為證明該程式的正確性, 依舊定義集合:

設集P_{i} = { x | x為對於solve(i)的呼叫, 且x滿足本次solve(i)執行初始時刻choosn[0...choosn.size() - 1]嚴格遞增(如果choosn非空)且對應於{1,...,i - 1}的一個子集 }

    歸納起點: 對於集P_{n}中的元素, 直接觀察其程式碼邏輯可得: 若初始引數show為true, 則P_{n}中對應元素均可打印出當前choosn對應集合和集{n}的所有子集(包括空集)的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行中, 並於執行完畢後還原choosn為初始狀態, 若初始引數show為false, 則P_{n}中對應元素均可打印出當前choosn對應集合和集{n}的所有子集(不包括空集)的並集.

    遞推: 假設對於對於集P_{k} (2 =< k <= n)中的元素, 若初始引數show為true, 則P_{k}中對應元素均可打印出當前choosn對應集合和集{k,...,n}的所有子集(包括空集)的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行, 並於執行完畢後還原choosn為初始狀態. 若初始引數show為false, 則P_{k}中對應元素均可打印出當前choosn對應集合和集{k,...,n}的所有子集(不包括空集)的並集. 那麼對於P_{k - 1}中的任意元素, 其是否列印初始choosn對的應集合, 可由第9行正確處理, 將所有choosn對應集合和集{k - 1,...,n}的所有子集(不包括空集)的並集分為兩類, 第一類包含元素k - 1, 第二類不包含k - 1, 易知第二類所有集合的字典序大於第一類所有集合, 因此程式第13行先列印第一類集合, 第14行後列印第二類集合是正確的,據此對於集合P_{k - 1}中的元素, 若初始引數show為true, 則P_{k - 1}中對應元素均可打印出當前choosn對應集合和集{k - 1,...,n}的所有子集(包括空集)的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行, 並於執行完畢後還原choosn為初始狀態. 若初始引數show為false, 則P_{k - 1}中對應元素均可打印出當前choosn對應集合和集{k - 1,...,n}的所有子集(不包括空集)的並集.

   綜上述: 對於集P_{1}中的元素, 若初始引數show為true, 則P_{1}中對應元素均可打印出當前choosn對應集合和集{1,...,n}的所有子集(包括空集)的並集, 且滿足同一個子集中的元素按遞增順序列印在同一行, 並於執行完畢後還原choosn為初始狀態. 若初始引數show為false, 則P_{1}中對應元素均可打印出當前choosn對應集合和集{1,...,n}的所有子集(不包括空集)的並集, 也即solve(1, true)按字典序遞增列印集合{1,...,n}的所有子集(包括空集), 從而程式正確性得證.