递归_CH0301_递归实现指数型枚举_递归算法正确性证明范例

版权声明:本文为博主原创作品, 转载请注明出处! https://blog.csdn.net/solider98/article/details/83273327

点此打开题目页面

    简而言之本题要求打印集{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}的所有子集(包括空集), 从而程序正确性得证.

猜你喜欢

转载自blog.csdn.net/solider98/article/details/83273327