专题-搜索&模拟
前言:
被某位小学妹搞得写不进去题了,想上吊。。。所以我来水题解啦~~因为都是洛谷原题,我就不粘题面啦。
\(ps:\) 是按照我写题的顺序写的题解,不是按照原本题目的排列顺序写的!
\(T1:\)小木棍
思路:
老熟题啦!看看今天的标题也知道这道题用什么解法。首先,我们可以确定\(ans\)一定大于等于这几段小木棍的最大值,小于等于这些木棍长度之和。然后考虑剪枝:显然最小长度一定得是总长度的因数。然后我们开始进行搜索,具体实现细节见代码注释哦~~
代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[70],sum,ans,minn=1e9,maxn,cnt[51];
inline void dfs(int num,int m,int s){//剩余木棍数量 剩余木棍最大值 目前这一组的长度和
if(num==0){//搜丸啦~~
cout<<ans;//输出答案
exit(0);//强制结束
}
if(s==ans){
dfs(num-1,maxn,0);//这一组满啦,该下一组啦。
return;
}
if(m==minn-1) return;//最大值小于最小值?违背人类伦理,结束结束。
for(int i=m;i>=minn;i--){//倒序遍历木棍长度
if(cnt[i]&&i+s<=ans){//如果存在且符合题意
--cnt[i];//收缴收缴!
dfs(num,i,i+s);//继续搜索
++cnt[i];//记得回溯~~
if(s==0||s+i==ans) break;//如果能回溯回来肯定证明之前出bug了,结束结束!
}
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];
++cnt[a[i]];
minn=min(minn,a[i]);//最小值
maxn=max(maxn,a[i]);//最大值
}
for(int i=maxn;i<sum;i++) //遍历可能长度
if(sum%i==0){//剪枝
ans=i;记录答案
dfs(sum/i,maxn,0);//搜索
}
cout<<sum;//都不符合条件,只能是木棍长度之和了。
return 0;
}
\(T2:\)买瓜
思路:
当然还是使用折半搜索。将所有瓜分为前一半和后一半,分别进行搜索。对于一个瓜,我们可以分为三种状态:全选,全不选,选一半(要劈开的喔)。大体思路就这样愉快的结束啦~~ 其他的具体细节键代码叭~~
代码:
#include<iostream>
#include<map>
#include<algorithm>
#define int long long
using namespace std;
const int N=1e7+10;
int n,ans=1e18;
double m,a[N];
map<double,bool> flag; //存储当前值是否搜到过
map<double,int> step;//step[i]为变成i最小的次数
inline void dfs(double sum,int id,int cnt){
if(sum>m||id>n/2) return ;//判边界
if(step[sum]<cnt&&flag[sum]) return ; //若原来的次数比现在快不用再搜索了
if(sum==m) { //若组合出了直接更新
ans=min(ans,cnt);
return ;
}
step[sum]=cnt;flag[sum]=1;//存储
dfs(sum+a[id+1],id+1,cnt);
dfs(sum,id+1,cnt);
dfs(sum+a[id+1]/2,id+1,cnt+1);//三种状态
}
inline void DFS(double sum,int id,int cnt){
if(sum>m||id>n) return ; //判边界
if(step.count(m-sum)){//若原来能组成出m-sum
ans=min(ans,cnt+step[m-sum]); //更新
return ;
}
DFS(sum+a[id+1],id+1,cnt);
DFS(sum,id+1,cnt);
DFS(sum+a[id+1]/2,id+1,cnt+1);//三种状态
}
signed main(){
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n);
dfs(0,0,0);DFS(0,n/2,0);//前n/2 后 n-n/2 个
cout<<(ans>1e18/2?-1:ans)<<'\n'; //判无解
return 0;
}
\(T3:\) [NOIP2015 提高组] 斗地主 加强版
思路:
这道题的主流思想有两个:四维\(dp\)和超大模拟。显然那个超大模拟码量太大,作为一个懒人,我果断选择了四维\(dp\)。(一会去给你们贺一个大模拟的链)。在这里把出牌方式分为顺子和其他(四带二,三带一,三代二等等)。这里用搜索处理顺子,其他则是用\(dp\)预处理出来,状态为\(dp[a][b][c][d]\)表示有\(a\)个单张牌\(b\)组对\(c\)个三张相同牌\(d\)个炸弹。其他具体细节还是见代码哦~
\(ps:\) 这里规定数码相同的牌为同一种牌。
代码:
#include<iostream>
#include<cstring>
using namespace std;
int T,n,y,a,b,ans,c[5],s[15],dp[30][15][10][8];
inline void dfs(int x){//搜索 出x次牌
if(x>=ans) return ;//已经比之前的答案要大 那一定不优。
memset(c,0,sizeof(c));//清空
for(int i=2;i<=14;i++) c[s[i]]++;//记录一下为几顺子(单顺子,双顺子,三顺子...)
ans=min(ans,x+dp[c[1]+s[0]][c[2]][c[3]][c[4]]);//有一只鬼
if(s[0]==2) ans=min(ans,x+dp[c[1]][c[2]][c[3]][c[4]]+1);//有双鬼
for(int a=1;a<=3;a++){//枚举一下几顺子
for(int i=3;i<=14;i++){//枚举开始的位置(A被设为了14,2不能参与顺子,所以从3开始)
int cnt=0;//记录能连几个
for(int j=i;j<=14;j++){//枚举可能的结束位置
if(s[j]>=a) cnt++;//结束的位置有足够的牌
else break;
if(a*cnt>=5){//充分发挥人类智慧,发现a*cnt至少大于等于5是符合顺子的要求
for(int k=i;k<=j;k++) s[k]-=a;//打出a顺子
dfs(x+1);//搜索
for(int k=i;k<=j;k++) s[k]+=a;//回溯
}
}
}
}
}
signed main(){
ios::sync_with_stdio(false);
//因为至多有23张牌,所以炸弹,对子等都有一定的数量限制
for(int d=0;d<=5;d++){//炸弹的数量
for(int c=0;c<=8;c++){//三张牌的数量
for(int b=0;b<=12;b++){//对子的数量
for(int a=0;a<=23;a++){//单张牌的数量
if(!a&&!b&&!c&&!d) continue;//没有牌
y=0x3f3f3f3f;//初始化
if(a) y=min(y,dp[a-1][b][c][d]+1);//出单张牌
if(b) y=min(y,dp[a][b-1][c][d]+1);//出一个对
if(c) y=min(y,dp[a][b][c-1][d]+1);//出一个三张牌
if(d) y=min(y,dp[a][b][c][d-1]+1);//出一个炸弹
if(a&&c) y=min(y,dp[a-1][b][c-1][d]+1);//出一个三带一
if(b&&c) y=min(y,dp[a][b-1][c-1][d]+1);//出一个三带二
if(a>=2&&d) y=min(y,dp[a-2][b][c][d-1]+1);//出一个四带两单
if(b>=2&&d) y=min(y,dp[a][b-2][c][d-1]+1);//出一个四带两对
if(b) y=min(y,dp[a+2][b-1][c][d]);//把一个对拆违两张单牌
if(c) y=min(y,dp[a+1][b+1][c-1][d]);//把一个三张牌拆为一张单牌加一个对子
if(d) y=min(y,dp[a+1][b][c+1][d-1]);//把炸弹拆为一个三张牌加一张单牌
if(d) y=min(y,dp[a][b+2][c][d-1]);//把炸弹拆为两个对子
dp[a][b][c][d]=y;//记录答案
}
}
}
}
cin>>T>>n;
while(T--){
memset(s,0,sizeof(s));//多组数据清空
for(int i=1;i<=n;i++){
cin>>a>>b;
if(a==1) a=14;//为了方便找顺子
s[a]++;//记录每种牌的数目
}ans=0x3f3f3f3f;
dfs(0);
cout<<ans<<'\n';
}
return 0;
}
\(T4:\)Anya and Cubes
思路:
这个搜索的思路还是蛮好想的,对于一个数有三种状态:选,不选,变为阶乘。不过可能有人一看到\(1e9\)的阶乘就慌了(比如我),莫慌莫慌,再偷偷看一眼数据范围:嗯,\(s≤10^{16}\),大概估算一下,能变为阶乘的数最大超不过20,那好办了,直接与处理呗。然后就没有什么难点了,其他的还是见代码哦~~
代码:
#include<iostream>
#include<algorithm>
#include<unordered_map>
#define int long long
using namespace std;
const int N=30;
int n,k,s,ans,a[N],fac[25];
unordered_map<int,int> num[N];//用map会被卡!!!
inline void dfs(int sum,int id,int cnt){//当前和 当前遍历到第id位 共用了cnt个!
if(sum>s||cnt>k) return ;//当前和大于总和或当前阶乘数大于总阶乘数
if(id>n/2){//遍历了一半
num[cnt][sum]++;//记录一下
return ;
}
dfs(sum+a[id],id+1,cnt);//选这个数
dfs(sum,id+1,cnt);//不选这个数
if(a[id]<=20) dfs(sum+fac[a[id]],id+1,cnt+1);//把它变成阶乘
}
inline void DFS(int sum,int id,int cnt){
if(sum>s||cnt>k) return ;//同10
if(id>n){//tong11
for(int i=0;i<=k-cnt;i++) ans+=num[i][s-sum]; //如果前半程有能与当前情况匹配成答案的加上
return ;
}
DFS(sum+a[id],id+1,cnt);//选数
DFS(sum,id+1,cnt);//不选数
if(a[id]<=20) DFS(sum+fac[a[id]],id+1,cnt+1);//阶乘
}
signed main(){
ios::sync_with_stdio(false);
fac[1]=1;
for(int i=2;i<=20;i++) fac[i]=fac[i-1]*i;//预处理阶乘
cin>>n>>k>>s;
for(int i=1;i<=n;i++) cin>>a[i];
dfs(0,1,0);DFS(0,n/2+1,0);//折半搜索
cout<<ans<<'\n';
return 0;
}
\(T5:\)Expression
思路:
乍一看没有思路,再一看还是没有思路。那整体看没法做只能一位一位的看喽。既然放在这了肯定是折半搜索,怎么搜呢?从个位开始往前搜,也分为三种情况:给\(x\)加数,给\(y\)加数,给\(z\)加数。具体实现还是见代码呗~~
\(ps:\)这题搜索传的参比较多,注意不要调错参数,不然你会怒调一小时无果。
代码(先别急着打,这是假的!):
#include<iostream>
#define int long long
using namespace std;
int x,y,z,ansa,ansb,ansc,ans=0x3f3f3f3f,p[20];char ch;
inline void dfs(int x,int y,int z,int carry,int a,int b,int c,int len,int dep){
//现在的x,现在的y,现在的z,进位,答案的a,答案的b,答案的c,总答案的长度,数字位数
if(len>=ans) return ;//如果之前有更优解则这次搜索肯定不优
if(!x&&!y&&!z&&!carry){//已经加完了
ans=len;//记录长度
ansa=a;//记录a的答案
ansb=b;//记录b的答案
ansc=c;//记录c的答案
return ;
}
if(!z){//c的最高位已经没有了
int res=x+y+carry;//c上还差的数
int cnt=0;//记录位数
while(res){
cnt++;
res/=10;
}
dfs(0,0,0,0,a+p[dep]*x,b+p[dep]*y,c+p[dep]*(x+y+carry),len+cnt,dep+1);//都加上去继续搜索
return ;
}
if((x+y+carry)%10==z%10){//三个数个位符合人类逻辑
dfs(x/10,y/10,z/10,(x%10+y%10+carry)/10,a+(x%10)*p[dep],b+(y%10)*p[dep],c+(z%10)*p[dep],len,dep+1);//舍弃个位继续向前搜
return ;
}
dfs(x*10+(z%10-y%10-carry+10)%10,y,z,carry,a,b,c,len+1,dep);//给x上加数
dfs(x,y*10+(z%10-x%10-carry+10)%10,z,carry,a,b,c,len+1,dep);//给y上加数
dfs(x,y,z*10+(x%10+y%10+carry)%10,carry,a,b,c,len+1,dep);//给z上加数
}
signed main(){
cin>>x>>ch>>y>>ch>>z;
p[0]=1;
for(int i=1;i<=18;i++) p[i]=p[i-1]*10;//初始化位数
dfs(x,y,z,0,0,0,0,0,0);//搜索
cout<<ansa<<"+"<<ansb<<"="<<ansc;
return 0;
}
\(update:2025/08/23\)
\(ps:\)有个同机房大佬发现了代码漏洞,几乎把整个机房都给\(hack\)了。
\(hack\)数据:\(2+4324=53245\)
应得答案:\(2+543243=543245\)
假代码答案:\(10002+43243=53245\)
问题出在哪了呢?我们只是往前搜非零的数了,但似乎并没有考虑两个非零数之间有否有零,有几个零的问题。不过也不是什么大问题,只要在原本就多的参数上雪上加霜一下就好了。再加两个新的参数\(la,lb\)来分别表示我们给\(x,y\)的前面加上的零的个数。
新代码:
#include<iostream>
#define int long long
using namespace std;
int x,y,z,ansa,ansb,ansc,ans=0x3f3f3f3f,p[20];char ch;
inline void dfs(int x,int y,int z,int carry,int a,int b,int c,int len,int dep,int la,int lb){
if(len>=ans) return ;
if(!x&&!y&&!z&&!carry){
ans=len;
ansa=a;
ansb=b;
ansc=c;
return ;
}
if(!z){
int res=x+y+carry;
int cnt=0;
while(res){
cnt++;
res/=10;
}
dfs(0,0,0,0,a+p[dep]*x,b+p[dep]*y,c+p[dep]*(x+y+carry),len+cnt,dep+1,la,lb);
return ;
}
if((x+y+carry)%10==z%10){
if(!x) la++;
if(!y) lb++;
dfs(x/10,y/10,z/10,(x%10+y%10+carry)/10,a+(x%10)*p[dep],b+(y%10)*p[dep],c+(z%10)*p[dep],len,dep+1,la,lb);
return ;
}
dfs(x*10+(z%10-y%10-carry+10)%10,y,z,carry,a,b,c,len+1+la,dep,0,lb);
dfs(x,y*10+(z%10-x%10-carry+10)%10,z,carry,a,b,c,len+1+lb,dep,la,0);
dfs(x,y,z*10+(x%10+y%10+carry)%10,carry,a,b,c,len+1,dep,la,lb);
}
signed main(){
cin>>x>>ch>>y>>ch>>z;
p[0]=1;
for(int i=1;i<=18;i++) p[i]=p[i-1]*10;
dfs(x,y,z,0,0,0,0,0,0,0,0);
cout<<ansa<<"+"<<ansb<<"="<<ansc;
return 0;
}
\(T6:\)[abc336_f]Rotation Puzzle
思路: 未完...
代码:
#include<bits/stdc++.h>
using namespace std;
int a[9][9],b[9][9],n,m;
unordered_map<unsigned long long,int> mp1,mp2;
inline unsigned long long hhash(int a[9][9])
{
unsigned long long h = 0;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
h = h*7654321 + a[i][j];
return h;
}
inline unsigned long long hashh(int a[9][9])
{
unsigned long long h = 0;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
h = h*1234567 + a[i][j];
return h;
}
inline void rotate(int x,int y)
{
memcpy(b,a,sizeof(a));
for(int i = 0;i <= n-2;i++)
for(int j = 0;j <= m-2;j++)
b[i+x][j+y] = a[n-2-i+x][m-2-j+y];
memcpy(a,b,sizeof(b));
}
int ans = 100;
inline void dfs1(int step)
{
unsigned long long h1 = hhash(a),h2 = hashh(a);
if(!mp1[h1]) mp1[h1] = step-1;
else mp1[h1] = min(mp1[h1],step-1);
if(!mp2[h2]) mp2[h2] = step-1;
else mp2[h2] = min(mp2[h2],step-1);
if(step > 10) return;
rotate(1,1);
dfs1(step+1);
rotate(1,1);
rotate(1,2);
dfs1(step+1);
rotate(1,2);
rotate(2,1);
dfs1(step+1);
rotate(2,1);
rotate(2,2);
dfs1(step+1);
rotate(2,2);
}
inline void dfs2(int step)
{
unsigned long long h1 = hhash(a),h2 = hashh(a);
if(mp1[h1] && mp2[h2]) ans = min(ans,mp1[h1]+step-1);
if(step > 10) return;
rotate(1,1);
dfs2(step+1);
rotate(1,1);
rotate(1,2);
dfs2(step+1);
rotate(1,2);
rotate(2,1);
dfs2(step+1);
rotate(2,1);
rotate(2,2);
dfs2(step+1);
rotate(2,2);
}
int main()
{
cin >> n >> m;
bool flag = 1;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
{
cin >> a[i][j];
if(a[i][j] != (i-1)*m+j) flag = 0;
}
if(flag)
{
cout << 0 << endl;
return 0;
}
dfs1(1);
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
a[i][j] = (i-1)*m + j;
dfs2(1);
if(ans > 20) cout << -1;
else cout << ans;
return 0;
}
\(T7:\)Distinct Paths
思路:未完...
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=35,mod=1e9+7;
int n,m,k,c[N],a[N][N],dp[N][N];
int dfs(int x,int y){//dfs使用int类型方便记录答案
if(x==n+1) return 1;
int st=dp[x-1][y]|dp[x][y-1];
if(n+m-x-y+1>k-__builtin_popcount(st))return 0;//剪枝1:可行性剪枝
int tmp=-1,res=0;//tmp记录第一次搜索的答案,res统计总方案数
for(int i=1;i<=k;i++){//i枚举该格子填充的颜色
if(a[x][y]&&a[x][y]!=i)continue;//该格子已经被钦定颜色
if(st&(1<<i-1))continue;//之前已经填充过i这个颜色,不能重复填
dp[x][y]=st|(1<<i-1),c[i]++;//可以填i
if(c[i]==1){//如果是剩余的颜色(在后面没有出现过(被钦定过))
if(tmp==-1)tmp=(y==m?dfs(x+1,1):dfs(x,y+1));//第一个这种颜色,搜索并记录答案tmp
res+=tmp,res%=mod;//不是第一次的话直接加答案就行了
}else res+=(y==m?dfs(x+1,1):dfs(x,y+1)),res%=mod;
c[i]--;//回溯
}
return res;
}
signed main(){
cin>>n>>m>>k;
if(n+m-1>k){cout<<0;return 0;}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j],c[a[i][j]]++;//c:辅助第二个剪枝
cout<<dfs(1,1);
return 0;
}