最近刷题,如果是深搜,往往是剪枝比较烦,有些甚至还要卡输入输出,不过做到的题大多是重在剪枝,深搜一般是从头开始的枚举这种无脑套路,关于剪枝,主要是剖析问题,大胆剪枝,没什么好说的,想不到的就想不到吧。
昨天刷到的一道题,倒是刷新了我对深搜方式的认识,(ps:看到网上讲解,说这题是玄学剪枝,不是很同意)因为这题重点不在剪枝,重在怎么深搜。
下面给题:
虫食算
题目描述
所谓虫食算,就是原先的算式中有一部分被虫子啃掉了,需要我们根据剩下的数字来判定被啃掉的字母。来看一个简单的例子:
43#9865#045
+8468#6633
44445509678
其中#号代表被虫子啃掉的数字。根据算式,我们很容易判断:第一行的两个数字分别是5和3,第二行的数字是5。
现在,我们对问题做两个限制:
首先,我们只考虑加法的虫食算。这里的加法是N进制加法,算式中三个数都有N位,允许有前导的0。
其次,虫子把所有的数都啃光了,我们只知道哪些数字是相同的,我们将相同的数字用相同的字母表示,不同的数字用不同的字母表示。如果这个算式是N进制的,我们就取英文字母表午的前N个大写字母来表示这个算式中的0到N-1这N个不同的数字:但是这N个字母并不一定顺序地代表0到N-1)。输入数据保证N个字母分别至少出现一次。
BADC
CBDA
DCCC 上面的算式是一个4进制的算式。很显然,我们只要让ABCD分别代表0123,便可以让这个式子成立了。你的任务是,对于给定的N进制加法算式,求出N个不同的字母分别代表的数字,使得该加法算式成立。输入数据保证有且仅有一组解
输入
包含四行。第一行有一个正整数N(N<=26),后面的3行每行有一个由大写字母组成的字符串,分别代表两个加数以及和。这3个字符串左右两端都没有空格,从高位到低位,并且恰好有N位。
输出
对于全部的数据,保证有包含一行。在这一行中,应当包含唯一的那组解。解是这样表示的:输出N个数字,分别表示A,B,C……所代表的数字,相邻的两个数字用一个空格隔开,不能有多余的空格。
样例输入
5
ABCED
BDACE
EBBAA
样例输出
1 0 3 4 2
题解:
剪枝的两个点很容易看出。
1、A,B首位相加不能进位
2、若A,B,C的第i位已知,(A+B)%n 或(A+B+1)%n 不低于 C,则不成立(+1是考虑进位情况)
最后的总判也很简单,从末到首,存在相加不等则不成立。
下面赋上我一开始的代码:
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
int n,a[30],b[30],c[30];
bool flag=0;//总判断
bool f[30];//表示i这个数是否被用过
int num[30];//num[i]表示i这个字母表示的数
ll sum;
bool judge(int x){
for(int i=0;i<n;i++){
if(a[i]<x&&b[i]<x&&c[i]<x){
//第i位的三个数都取到值时
int t1=num[a[i]]+num[b[i]];
int t2=t1+1; //考虑进位
int t3=num[c[i]];
t1%=n;
t2%=n;
if(t1!=t3&&t2!=t3) return 1; //t1,t2若都不等于t3,则不成立
}
}
return 0;
}
bool add(){
int t=0; //用于进位
for(int i=n-1;i>=0;i--){
int temp=num[a[i]]+num[b[i]]+t;
if(temp%n!=num[c[i]]) return 0;
t=temp/n;
}
return 1;
}
void dfs(int len){
//深搜结束
if(len==n){
if(add()){
//相加判断
flag=1;
return;
}
return;
}
//剪枝一
if(a[0]<len&&b[0]<len){
//首位都取到值时
if(num[a[0]]+num[b[0]]>=n) return; //首位相加若进位,则不成立。
}
//剪枝二
if(judge(len)) return;
//剪枝三
if(flag) return;
//深搜继续
for(int i=n-1;i>=0;i--){
//从高位开始取
if(flag) return;
if(!f[i]){
num[len]=i;
f[i]=1;
sum++;
dfs(len+1);
f[i]=0;
}
}
}
int main(){
//输入并转化
cin>>n;
getchar();
for(int i=1;i<=3;i++){
string temp;
getline(cin,temp);
if(i==1) for(int j=0;j<n;j++) a[j]=temp[j]-'A';
else if(i==2) for(int j=0;j<n;j++) b[j]=temp[j]-'A';
else if(i==3) for(int j=0;j<n;j++) c[j]=temp[j]-'A';
}
dfs(0);
//输出
for(int i=0;i<n;i++){
if(i==0) cout<<num[i];
else cout<<' '<<num[i];
}
cout << endl << sum << endl;
}
首先思路很清晰可以看出。(下面给出化简版)
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
bool flag=0;//总判断
bool judge(int x){
}
bool add(){
}
void dfs(int len){
//深搜结束
if(len==n){
}
//剪枝一
if(a[0]<len&&b[0]<len){
}
//剪枝二
if(judge(len)) return;
//剪枝三
if(flag) return;
//深搜继续
for(int i=0;i<n;i++){
}
}
int main(){
//输入并转化
cin>>n;
for(int i=1;i<=3;i++){
}
dfs(0);
//输出
for(int i=0;i<n;i++){
}
//cout << endl << sum << endl;
}
这里我的深搜方式,是从头开始枚举,先给A赋值,再B,C,D等等。判断一个字母是否被赋值,只要看深搜深度len。
可是这样超时。
答案给的深搜方式不是这种无脑流,他多了下面一个操作。
for(int i=n-1;i>=0;i--){
//记录搜索的字母顺序
if(!f[a[i]]) arr[cnt++]=a[i],f[a[i]]=1;
if(!f[b[i]]) arr[cnt++]=b[i],f[b[i]]=1;
if(!f[c[i]]) arr[cnt++]=c[i],f[c[i]]=1;
if(cnt==n) break;
}
//for(int i=0;i<n;i++) printf("%d ",arr[i]);
不是按递增顺序,而是按字母出现的顺序,比如:案例一中,先给D赋值,再E,A,B,C, A (重复,省略)等等,从后往前(从前往后也可以),这样操作后,赋三个值后,就可以判断剪枝二,原来的方法至少赋值3个,最多n个,
5
ABCED
BDACE
EBBAA
接来下比较一下复杂度,因为不好看出,我用深搜的次数来看看,
案例一中,原方法回溯76次,而这个12次。
自己有凑了个案例二
7
ABCDEFG
GCABGAF
AEDFFGF
1 2 3 4 5 6 0
原方法回溯2101次,而这个88次。
变换深搜顺序后,相应代码也可以稍微改改,下面附上AC代码
#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
using namespace std;
int n,cnt;
int a[30],b[30],c[30];
bool f[30];//表示i这个数是否被用过
int num[30];//num[i]表示i这个字母表示的数
int arr[30];
bool flag;
//ll sum;
bool judge(){
for(int i=0;i<n;i++){
int A=num[a[i]],B=num[b[i]],C=num[c[i]];
if(A!=-1&&B!=-1&&C!=-1){
//第i位的三个数都取到值时
int t1=(A+B)%n;
int t2=(A+B+1)%n; //考虑进位
if(t1!=C&&t2!=C) return 1; //t1,t2若都不等于C,则不成立
}
}
return 0;
}
bool add(){
int t=0; //用于进位
for(int i=n-1;i>=0;i--){
int temp=num[a[i]]+num[b[i]]+t;
if(temp%n!=num[c[i]]) return 0;
t=temp/n;
}
return 1;
}
void dfs(int len){
//搜索第len个字母是哪个数
//深搜结束
if(len==n){
if(add()){
//相加判断
flag=1;
return;
}
return;
}
//剪枝一
if(num[a[0]]+num[b[0]]>=n) return;
//剪枝二
if(judge()) return;
//剪枝三
if(flag) return;
for(int i=n-1;i>=0;i--){
if(!f[i]){
num[arr[len]]=i;
f[i]=1;
// sum++;
dfs(len+1);
if(flag) return;
f[i]=0;
num[arr[len]]=-1;
}
}
}
int main(){
//输入并转化
cin>>n;
for(int i=1;i<=3;i++){
string temp;
cin>>temp;
if(i==1) for(int j=0;j<n;j++) a[j]=temp[j]-'A';
else if(i==2) for(int j=0;j<n;j++) b[j]=temp[j]-'A';
else if(i==3) for(int j=0;j<n;j++) c[j]=temp[j]-'A';
}
for(int i=n-1;i>=0;i--){
//记录搜索的字母顺序
if(!f[a[i]]) arr[cnt++]=a[i],f[a[i]]=1;
if(!f[b[i]]) arr[cnt++]=b[i],f[b[i]]=1;
if(!f[c[i]]) arr[cnt++]=c[i],f[c[i]]=1;
if(cnt==n) break;
}
//for(int i=0;i<n;i++) printf("%d ",arr[i]);
memset(f,false,sizeof(f));
memset(num,-1,sizeof(num));
dfs(0);
for(int i=0;i<n;i++){
if(i==0) cout<<num[i];
else cout<<' '<<num[i];
}
// cout << endl << sum << endl;
}