从简单背包加密中再谈搜索与动态规划


题目说明

问题:已知一背包加密的公钥为{615436700291,415460700271,15508700231,846430100773,677471501215, 139578302079,179168604148,789306608798,563224517265,364498233536,229056467022,670323428329, 115934481316,44989786476,518624653302,149955258190,728568829281,796899516776,546782575075, 178164449829,356328899658,712657799316,569303048254,223205396187,446410792374,892821584748, 524144817108,132888933895,611875519857,877653387647,839906074973,35774353074}, 密文为6020587936087,试求明文二进制表示(形如:01001010000001110101001100001110)。 真正的背包加密要复杂得多,而且有固定的解法。这个是我们就题论题,首先我们这个问题是面向用例编程,和OJ不太一样,我们是用方法来解决这一个特殊问题。先做预处理,并初始化long long数组a

#include<iostream>
#include<vector>
#define WEIGHT 6020587936087
using namespace std;
typedef long long ll;

int n;

思考

首先看到“背包”两字就非常敏感,妥妥的动态规划,而且有且仅有一个解满足条件,天哪,这不正好。我想是不是这么个情况。这是一个存在性背包问题,我们可以通过两重循环在\(\large O \normalsize \left( n \times MAX\right)\)的时间内完成,并且我们可以找一个pre数组来记录前一个取值,很轻松地找到那组可行解,好耶,这种做法相当研究生。那么我们思考一下写出动态规划的代码。


动态规划

vector<ll> a = { 615436700291,415460700271,15508700231,846430100773,677471501215,139578302079,179168604148,789306608798,563224517265,364498233536,229056467022,670323428329,115934481316,44989786476,518624653302,149955258190,728568829281,796899516776,546782575075,178164449829,356328899658,712657799316,569303048254,223205396187,446410792374,892821584748,524144817108,132888933895,611875519857,877653387647,839906074973,35774353074 };
int size = a.size(); //确定物品数量
bool* dp = new bool[WEIGHT + 1]; //dp数组下标意义为重量
unordered_map<ll, pair<ll, int>> premp; //使用哈希表求沿途的元素,对应映射为:该重量对应前一重量和填入背包的元素
for (int j = 0; j < size; ++j) //由于物品只能使用一次,外层遍历物品,内层遍历重量
	for (ll i = 0; i < WEIGHT; ++i)
		if ((i == a[j]) || (i >= a[j] && dp[i - a[j]] == true)) //使得i能被拼出的条件:直接有对应的物品,或(i - 某一物品重量)能被拼出
		{
			dp[i] = true;
			premp[i].first = a[j];
			premp[i].second = j; //确定倒序查找的线索
		}
vector<bool> taken(size, 0);
int j = 0;
for (ll i = WEIGHT; i == premp[i].second; i = premp[i].first, j = premp[i].second) //倒序寻找加入背包的元素
	taken[j] = true;
taken[j] = true;
for (int i = 0; i < size; ++i) //输出01序列
	cout << taken[i];
cout << endl;

当我们迫不及待的按下F5的时候会发现报错了,啊这为啥会有这么大的数组,我们看到WEIGHT是一个让人震惊的数字。你可能会说,我们干脆从\(\mathop{min}\limits_{i \in \left[0 , 31\right]}\left\{ a_{i} \right\}\)开始找,但是依然是一个很大的数字,诶,这个题动态规划失效了。当然还有其他的方法优化处理,楼主菜鸡表示不会。


搜索

对问题进行转化,实际上这是一个求子集合的问题,即找到某一集合,使得各元素和等于所给值sum,并指明该集合。而且我们注意这道题数组的大小为32,对于任何一个元素,拿与不拿是两种情况可分别搜索,比照嗯二叉搜索的时间复杂度\(\large O \normalsize \left(2^{n}\right)\),我们嗯穷举的量大概是\(2^{32}\),也就是\(10^{10}\),这个量还是非常猛,但是至少可以让程序run起来。 顺着这个思路我们先稍微写一下朴素的搜索算法,大概像这样。

void backtracking(int cur, ll sum, vector<bool> istaken, vector<ll> a)
{
	if (cur == n - 1)
	{
		if (sum == WEIGHT)
		{
			for (int i = 0; i < n; ++i)
				cout << istaken[i];
			cout << endl;
		}
		return;
	}

	istaken[cur] = true;
	backtracking(cur + 1, sum + a[cur], istaken, a);
	istaken[cur] = false;
	backtracking(cur + 1, sum, istaken, a);
}

我们注意到搜索的终极大法是剪枝,有枝可以剪吗,答案是肯定的,一旦我们当前和已经超过了最大可容纳重量WEIGHT,那又没有负数,肯定要把背包撑爆,不会成为我们的最终选择,那我们干脆直接吧这种给淘汰掉。而且我们不难看出这个剪枝能使得效率提高很多。

#include<iostream>
#include<vector>
#define WEIGHT 6020587936087
	using namespace std;
	typedef long long ll;

	int n;

	/*
	函数名: backtracking
	参数说明:
	int cur : 表明当前递归层次,即当前考虑到数组中第cur + 1个元素
	long long sum : 进入函数时的当前和
	vector<bool> istaken : 记录加入背包的元素
	vector<long long> a : 物品重量数组
	int n : 全局变量,表明物品总量
	返回值: 无(隐含全局变量操作)
	*/

	void backtracking(int cur, ll sum, vector<bool> istaken, vector<ll> a)
	{
		if (cur == n - 1) //递归到最后一个物品时判断后回溯
		{
			if (sum == WEIGHT) //若能拼出WEIGHT,输出信息,也可以利用全局变量返回
			{
				for (int i = 0; i < n; ++i)
					cout << istaken[i];
				cout << endl;
			}
			return;
		}

		if (sum + a[cur] <= WEIGHT) //剪枝操作,若加入当前元素后重量已经超过WEIGHT,后面已经不可能拼出WEIGHT,不再向下递归
		{
			istaken[cur] = true; //先加入istaken
			backtracking(cur + 1, sum + a[cur], istaken, a); //从cur + 1开始,以sum + a[cur]为当前和,向下搜索
			istaken[cur] = false; //状态还原
		}
		if (sum <= WEIGHT) //剪枝操作,若当前重量超过WEIGHT,后面已经不可能拼出WEIGHT,不再向下递归
		{
			istaken[cur] = false; //纯属对应操作,可有可无
			backtracking(cur + 1, sum, istaken, a); //从cur + 1开始,以sum为当前和,向下搜索
		}
	}

	int main()
	{
		vector<ll> a = { 615436700291,415460700271,15508700231,846430100773,677471501215,139578302079,179168604148,789306608798,563224517265,364498233536,229056467022,670323428329,115934481316,44989786476,518624653302,149955258190,728568829281,796899516776,546782575075,178164449829,356328899658,712657799316,569303048254,223205396187,446410792374,892821584748,524144817108,132888933895,611875519857,877653387647,839906074973,35774353074 };
		n = a.size();
		vector<bool> istaken(n, 0);
		backtracking(0, 0, istaken, a); //注意开始搜索的条件

		return 0;
	}
所以面向用例编程yyds,emmm我们要看到数据的特点来选择恰当的方法

[toc]


文章作者: Commander
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Commander !
  目录