dfs回溯,bfs枚举,完全背包

P1036 P1021 P1032

P1036 选数

题目大意

已知 n n n 个整数,从中选出 k k k 个数进行相加,统计总和是素数的组合一共有多少种。

注意:

  1. n ≤ 20 n\le 20 n≤20,选数是组合,不考虑顺序;

  2. vis 只能保证:一条递归链条里面不会重复选同一个元素

    但是不能阻止不同顺序挑选同一批元素,产生重复方案。

  3. 同一个数字只能选一次;

  4. 不需要输出具体是哪些数,只输出满足条件的方案数量。

样例:

输入 (n=4,k=3),数字:3 7 12 19

所有3数组合:

(3+7+12=22)(不是素数)

(3+7+19=29)(素数)

(3+12+19=34)(不是素数)

(7+12+19=38)(不是素数)

满足条件只有1种,输出1


核心思路

DFS回溯枚举所有选 k k k个数的组合 + 素数判断

复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
const int N=101;
int n,k;
vector<int>a; 
//vector<bool>vis;
int ans=0;
bool check(int x){
	if(x==2)return true;
	else if(x<2)return false;
	else if(x%2==0)return false;
	else{
		for(int i=3;i*i<=x;i+=2){
			if(x%i==0)
			return false;
		}
	}
	return true;
}
//pos:当前遍历到数组第几个位置
//cnt:已经选了多少个数
//sum:已经选出数字的累加和
void dfs(int pos,int step,int sum){
	if(step==k){
		if(check(sum))
		ans++;
		return;
	}
	for(int i=pos;i<=n;i++){
		dfs(i+1,step+1,sum+a[i]);
		//vis[i]=false;
	}

}
void slove(){
	cin>>n>>k;
	a.resize(n+1);
	//vis.resize(n+1);
	ans=0; 
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}	
	dfs(1,0,0);
		cout<<ans<<endl;
}
signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int _=1;
	//cin>>_;
	while(_--)
	slove();
	return 0;
}

P1032 [NOIP 2002 提高组 字串变换(疑似错题) - 洛谷](https://www.luogu.com.cn/problem/P1032)

P1032 字串变换

题目大意

已知初始字符串 A A A,目标字符串 B B B,最多给出6条变换规则 A i → B i A_i \to B_i Ai→Bi:把字符串里的子串 A i A_i Ai替换成 B i B_i Bi。

每次变换,选取当前串中任意一处匹配 A i A_i Ai的子串,执行替换。

求从 A A A变为 B B B最少需要几步;步数不能超过10步(包含10) 。超过10步还没得到 B B B,输出 NO ANSWER!

样例:

A=abcd,B=xyz

规则:abc→xuud→yy→yz

变换:abcd → xud → xy → xyz,一共3步,输出3。

约束:所有字符串长度最大20,全部小写字母。


核心思路

最少变换步数 → 典型最短路问题,每一次字符串变换是一条边,字符串是节点,用==BFS广度优先搜索==。

  1. BFS队列存储:(当前字符串,已经变换的步数)
  2. unordered_set记录已经访问过的字符串,防止重复入队(死循环,例如a→b,b→a来回变换)。
  3. 每次取出队首元素:
    • 如果当前字符串等于目标 B B B,直接返回当前步数。
    • 如果步数 ≥ 10 \ge 10 ≥10,不再继续扩展,不能再往下走。
    • 遍历全部6条替换规则,对当前字符串,找到所有可以匹配该规则的子串位置,每一个位置替换生成新字符串。
    • 如果新字符串没有被访问过,标记访问,入队,步数+1。
  4. 队列为空还没找到目标串,输出NO ANSWER!

样例模拟

输入:

复制代码
start = "abcd",target = "xyz"
规则:
R1: "abc" → "xu"
R2: "ud" → "y"
R3: "y" → "yz"

队列:queue<{字符串,步数}>vis集合记录已经出现过的串。

允许最大步数:10。


初始状态

队列:[ ("abcd", 0) ]

vis = { "abcd" }

取出队首:str="abcd", stp=0

stp=0 < 10,可以扩展。

遍历全部3条规则:

  1. 规则R1:abc → xu
    ori="abc",长度3。
    枚举i:i +3 ≤4,i可取0,1
  • i=0:substr(0,3) = "abc"匹配成功
    新串:str[0,0] + "xu" + str[3:]"" + "xu" + "d""xud"
    "xud" != "xyz",不在vis。加入vis,入队 ("xud",1)
  • i=1:substr(1,3)="bcd"abc,不匹配。
  1. 规则R2:ud → y,ori长度2

    abcd中没有子串ud,无新串。

  2. 规则R3:y → yz

    abcd没有y,无新串。

当前队列:[ ("xud", 1) ]


取出队首:str="xud", stp=1

stp=1 <10。

遍历3条规则:

  1. R1 abc→xu:"xud"没有abc
  2. R2 ud→y,ori长度2
    i遍历:i+2 ≤3 → i=0,1
  • i=1:substr(1,2) = "ud"匹配成功
    新串:"x" + "y" + """xy"
    不等于目标xyz,未访问,入队 ("xy",2),vis加入"xy"
  1. R3 y→yz:此时"xud"没有字符y,不产生新串。

当前队列:[ ("xy", 2) ]


取出队首:str="xy", stp=2

stp=2<10。

遍历规则:

  1. R1:不匹配。
  2. R2:ud不存在。
  3. R3:y → yz,ori长度1。
    i遍历:i+1 ≤2,i=0,1
  • i=1:substr(1,1) = "y"匹配。
    新串:"x" + "yz""xyz"

✅新串"xyz"等于目标字符串!

返回 stp+1 = 2+1 = 3

复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'

struct Node{
    string s;
    int step;
};

vector<pair<string,string>> rule;
unordered_set<string> vis;

int bfs(string start,string target){
    queue<Node> q;
    q.push({start,0});
    vis.insert(start);

    while(!q.empty()){
        Node cur = q.front();
        q.pop();
        string str = cur.s;
        int stp = cur.step;

        if(stp >=10) continue; //最多允许10步,不再扩展

        //遍历每一条变换规则
        for(auto &r : rule){
            string ori = r.first;
            string rep = r.second;
            int lenOri = ori.size();
            //枚举所有可以替换的下标位置i
            for(int i=0;i+lenOri <= str.size();i++){
                if(str.substr(i, lenOri) == ori){
                    //i位置替换,生成新串
                    string ns = str.substr(0,i) + rep + str.substr(i+lenOri);
                    if(ns == target){
                        return stp+1;
                    }
                    if(!vis.count(ns)){
                        vis.insert(ns);
                        q.push({ns, stp+1});
                    }
                }
            }
        }
    }
    return -1;
}

void solve(){
    string A,B;
    cin>>A>>B;
    string x,y;
    while(cin>>x>>y){
        rule.emplace_back(x,y);
    }
    int ans = bfs(A,B);
    if(ans == -1){
        cout<<"NO ANSWER!"<<endl;
    }else{
        cout<<ans<<endl;
    }
}

signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    solve();
    return 0;
}

P1021 [NOIP 1999 提高组 邮票面值设计(疑似错题) - 洛谷](https://www.luogu.com.cn/problem/P1021)

P1021 邮票面值设计

题目大意

给定 N N N:最多可以贴 N N N 张邮票;

给定 K K K:一共要设计 K K K 种不同的邮票面值。(满足 N + K ≤ 15 N+K \le 15 N+K≤15)

邮票数量无限供应。要求选出 K K K 个面值,使得 1 ∼ M A X 1 \sim MAX 1∼MAX 的每一个邮资都可以用不超过 N N N 张邮票凑出来 。求最大的 M A X MAX MAX,同时输出选出来的面值(从小到大)。

样例:输入3 2

(N=3)最多贴3张,选2种面值1 3

(1,2,3,4,5,6,7) 全部都可以用不超过3张凑出来;8无法凑出。得到 (MAX=7)。

注意:面值必须包含1。如果没有面值1,数字1就凑不出来,直接MAX=0。


核心思路

算法组合:**DFS搜索枚举面值 +完全背包DP计算当前方案的连续最大邮资**

1.DFS部分

  • 搜索依次确定每一个邮票面值,面值严格递增,避免重复组合。
  • 剪枝:假设前面已经选出的面值可以连续凑到 m x mx mx,那么下一个新面值的取值范围:
    at-1+1 \le nextVal \le (mx+1)

如果新面值 >(mx+1),那么数字 (mx+1) 永远凑不出来,直接剪去这一分支。

  • 递归终止条件:已经选够 K K K 种面值。调用DP算出该方案的连续最大邮资,更新全局最优解。

2.DP(完全背包)

定义 (fi):凑邮资 i i i 需要的最少邮票张数

  1. 初始化:(fi)=i(全部用面值1去凑)
  2. 完全背包:对每一个已经选好的邮票面值,更新 f j = min ⁡ ( f j , f j − v a l + 1 ) fj=\min(fj,\ fj-val+1) fj=min(fj, fj−val+1)
  3. 从1开始往后遍历,找到第一个满足 (fi)>N 的下标,那么 i − 1 \boldsymbol{i-1} i−1 就是这套面值能连续到达的最大邮资MAX。

关键点

  1. DFS枚举所有合法面值组合,DP用来评估这套组合好不好。
  2. 剪枝nextVal ≤ \(mx+1\)是本题灵魂,大幅减少搜索量。
  3. 最少张数,不是求能否凑出;超过N张就算凑不出来。
  4. 不需要回溯数组:下一轮循环直接覆盖a[t]的值。

复杂度说明

N + K ≤ 15 N+K\le15 N+K≤15,加上强力剪枝,搜索量很小,可以直接暴力DFS。


算法流程总结

  1. dfs(t, mx):选第t个面值;mx是前面t-1个面值能连续到的最大邮资。
  2. 循环枚举下一个面值,范围at-1+1,mx+1
  3. 选定面值,调用dp函数得到新的连续上限new_mx
  4. 递归dfs(t+1, new_mx)
  5. 选满K个面值,对比更新全局最优解。

样例模拟 n=3,k=2

n:最多贴3张邮票;k:选2种面值

全局:a[]当前搜索面值,ans[]最优解,maxn最大连续邮资

初始:a[0]=0,调用 dfs(1, 0)


第一层 dfs( t=1 , mx=0 )

t=1:选第1个面值;mx=0:前面0个面值,连续最大为0

循环条件:

cpp 复制代码
i >= a[t-1]+1 = a[0]+1 = 1
i <= mx+1 = 0+1 =1

i只能等于1

  1. a[1]=1
  2. 调用 dp(t=1, mx=0),现在已选面值:{1}

dp(1,0)执行过程

t=1a[t]=a[1]=1,上限:a[t]*n =1*3=3

f数组大小到3,初始化:f[0]=0,f[1]=1,f[2]=2,f[3]=3

完全背包:只处理面值a[1]=1

复制代码
j从1~3
f[j] = min(f[j], f[j‑1]+1)

处理完后:

f[0]=0,f[1]=1,f[2]=2,f[3]=3

遍历找第一个f[i]>3

i=1 f=1;i=2 f=2;i=3 f=3;全部≤3。循环结束返回上限3

✅ dp返回 x=3

调用 dfs(t+1=2 , x=3)


第二层 dfs( t=2 , mx=3 )

t=2:选第2个面值;mx=3:前面{1}可以连续凑到3

循环条件:

cpp 复制代码
i >= a[t‑1]+1 = a[1]+1 = 2
i <= mx+1 =3+1 =4

i依次取:2,3,4

① i=2,a[2]=2

调用dp(t=2,mx=3),当前面值集合{1,2}

t=2a[t]=2;上限a[t]*n =2*3=6

初始化f[i]=if[0]=0,f1=1,f2=2,f3=3,f4=4,f5=5,f6=6

完全背包:

-先处理面值1:f[0~6]最少张数:0,1,2,3,4,5,6

-再处理面值2:

复制代码
j=2: min(2,f[0]+1)=1
j=3: min(3,f[1]+1)=2
j=4: min(4,f[2]+1)=2
j=5: min(5,f[3]+1)=3
j=6: min(6,f[4]+1)=3

最终f数组:

i 0 1 2 3 4 5 6
fi 0 1 1 2 2 3 3

逐个检查:

1:1,2:1,3:2,4:2,5:3,6:3;i=7超出循环上限。

dp返回 x=6

现在t=2 == k,进入下一层dfs(3,6)

dfs(t=3,mx=6)

t==k+1 (3==2+1)成立。

mx=6 > maxn(初始0)

更新maxn=6ans[1]=1 ans[2]=2。return回到上一层。


② i=3,a[2]=3

调用dp(t=2,mx=3),面值集合{1,3}

t=2a[t]=3;上限a[t]*n =3*3=9

初始化f[i]=i

完全背包:

  1. 面值1处理完:f[i]=i

  2. 处理面值3:

    j=3: min(3,f[0]+1)=1
    j=4: min(4,f[1]+1)=2
    j=5: min(5,f[2]+1)=3
    j=6: min(6,f[3]+1)=2
    j=7: min(7,f[4]+1)=3
    j=8: min(8,f[5]+1)=4
    j=9: min(9,f[6]+1)=3

f数组关键项:

i 1 2 3 4 5 6 7 8
fi 1 2 1 2 3 2 3 4

开始遍历:

i=1~7全部≤3;i=8时f[8]=4 > 3

✅ 第一个超3的i=8;dp返回x = i‑1 =7

调用dfs(3,7)

t ==k+1mx=7 > maxn(旧值6)

更新:maxn=7ans[1]=1 ans[2]=3。return。


③ i=4,a[2]=4

调用dp(t=2,mx=3),面值集合{1,4}

上限4*3=12

f数组:

1:1,2:2,3:3,4:1,5:2,6:3,7:4。

i=7,f7=4>3,dp返回x=6

调用dfs(3,6),mx=6,比当前maxn=7小,不更新最优解。return。

for循环全部i处理完毕,dfs(2,3)返回。

dfs(1,0)返回,搜索全部结束。


变量 含义

n 最多允许贴 n 张邮票

k 一共要选 k 种面值

a\[\] dfs 搜索过程中,当前正在尝试的面值数组

ans\[\] 保存最优的那一组面值

maxn 全局最大连续邮资 MAX

dp(t,mx) 传入已经选好t个面值,返回这套面值可以连续凑出的最大邮资

输出结果

复制代码
1 3
MAX=7

#include<bits/stdc++.h>
using namespace std;
int n,k,ans[20],a[20],maxn;

// dp(t,mx): 当前已经选了 t 个面值,计算这套面值能连续到的最大邮资
int dp(int t,int mx)
{
    int f[10000];
    //初始化:f[i]表示凑i最少需要多少张邮票,初始赋值i(最坏全部用1)
    for(int i=0;i<=a[t]*n;i++)
        f[i]=i;
    //完全背包
    for(int i=1;i<=t;i++)
    {
        for(int j=a[i];j<=a[t]*n;j++)
            f[j]=min(f[j],f[j-a[i]]+1);
    }
    //找第一个需要邮票数>n的位置
    for(int i=1;i<=a[t]*n;i++)
    {
        if(f[i]>n)
            return i-1;
    }
    return a[t]*n;
}

// dfs(t,mx): t表示当前要选第t个面值;mx:前面t?1个面值能够连续到的最大邮资
void dfs(int t,int mx)
{
    //递归终止:已经选够k个面值
    if(t==k+1)
    {
        if(mx>maxn)
        {
            maxn=mx;
            for(int i=1;i<=k;i++)
                ans[i]=a[i];
        }
        return;
    }
    /**
     * 剪枝循环:
     * a[t?1]+1:面值严格递增,比上一个面值大
     * mx+1:新增面值不能超过mx+1,否则mx+1这个数字凑不出来
    
    for(int i=a[t-1]+1;i<=mx+1;i++)
    {
        a[t]=i;
        int x=dp(t,mx); 
        dfs(t+1,x);
    }
}

int main()
{
    cin>>n>>k;
    a[0]=0;
    dfs(1,0);
    for(int i=1;i<=k;i++)
        cout<<ans[i]<<" ";
    cout<<endl;
    cout<<"MAX="<<maxn<<endl;
    return 0;
}
相关推荐
郝学胜_神的一滴1 小时前
干货版《算法导论》17:双数极值配对与卡牌手牌编码最优解
数据结构·算法
码工许师傅2 小时前
一文读懂《Effective C++ 第三版》的55条黄金法则
c++
有点。2 小时前
C++深度优先搜索(三)
开发语言·c++·深度优先
nike0good2 小时前
Codeforces Round 1101 (Div. 2) 题解
算法
中年阿甘2 小时前
解析式布局-二次线长布局
线性代数·算法·矩阵
程序喵大人2 小时前
【C++进阶】STL算法与函数对象 -【C++进阶】STL算法与函数对象
开发语言·c++·算法
我不会插花弄玉2 小时前
10.list【由浅入深-C++】
数据结构·c++·list
鹿角片ljp3 小时前
LeetCode 53. 最大子数组和
算法·leetcode·职场和发展
旖旎夜光3 小时前
C++(类与对象)(下)
开发语言·c++·学习