蓝桥杯真题Day46 倒计时7天真题+动态规划完全背包问题完结!!

[蓝桥杯 2016 省 B] 交换瓶子

题目描述

有N 个瓶子,编号1∼N,放在架子上。比如有 5 个瓶子:2,1,3,5,4要求每次拿起 2个瓶子,交换它们的位置。经过若干次后,使得瓶子的序号为:1,2,3,4,5 对于这么简单的情况,显然,至少需要交换 2 次就可以复位。如果瓶子更多呢?你可以通过编程来解决。

输入格式

第一行:一个正整数 N(N<10000),表示瓶子的数目。

第二行:N 个正整数,用空格分开,表示瓶子目前的排列情况。

输出格式

输出数据为一行一个正整数,表示至少交换多少次,才能完成排序。

代码表示

#include <bits/stdc++.h>
using namespace std;
 
int main() {
	int n;
    cin>>n;
    int pin[n];
    int count=0;
    for(int i=1;i<=n;++i){
    	cin>>pin[i];
    }
    for(int i=1;i<=n;++i){
    	if(pin[i]==i){
    		continue;
		}else{
			for(int j=2;j<=n;++j){
				if(pin[j]==i){
					swap(pin[i], pin[j]);
					count++;
					continue;	
				}
			}
		}
	}
    cout <<count;
    return 0;
}

心得体会

1、注意寻找的循环和查找的循环要分开写。

2、注意初始换循环变量的初始值的大小的设定。


[蓝桥杯 2018 省 B] 日志统计

题目描述

小明维护着一个程序员论坛。现在他收集了一份“点赞”日志,日志共有N 行。其中每一行的格式是 ts id,表示在ts 时刻编号id 的帖子收到一个“赞”。

现在小明想统计有哪些帖子曾经是“热帖”。如果一个帖子曾在任意一个长度为 D的时间段内收到不少于K 个赞,小明就认为这个帖子曾是“热帖”。

具体来说,如果存在某个时刻T 满足该帖在 [T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K 个赞,该帖就曾是“热帖”。给定日志,请你帮助小明统计出所有曾是“热帖”的帖子编号。

输入格式

第一行包含三个整数N、D 、K。

以下N 行每行一条日志,包含两个整数ts 和id。

输出格式

按从小到大的顺序输出热帖id。每个id 一行。

代码表示

第一种:超时/(ㄒoㄒ)/~~┭┮﹏┭┮

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    long long n, d, k;
    cin >> n >> d >> k;
    vector<long long> ts(n), id(n);
    for (int i = 0; i < n; ++i) {
        cin >> ts[i] >> id[i];
    }

    set<long long> hotPosts; // 使用set来存储热帖的id,确保唯一性和有序性

    for (int i = 0; i < n; ++i) {
        int count = 0;
        for (int j = 0; j < n; ++j) {
            if (id[i] == id[j] && abs(ts[j] - ts[i]) < d) {
                count++;
                if (count >= k) {
                    hotPosts.insert(id[i]);
                    break;
                }
            }
        }
    }
    for (set<long long>::iterator it = hotPosts.begin(); it != hotPosts.end(); ++it) 
	{
        cout << *it << "\n";
    }
    return 0;
}

 第二种

#include <bits/stdc++.h>
using namespace std;
 struct node {
    int ts, id;
} a[100005];

int n, d, k;

//这就是一个自定义排序函数,先用它对id排id相等用ts排序 
inline int cmp(node x, node y)
{
    if (x.id != y.id) {
        return x.id < y.id;
	}
    return x.ts < y.ts;
}

//检查从下标 x到 y的范围内是否满足热帖的条件
inline bool check(int x, int y) {
    int r = x;
    for (int i=x;i<=y;i++) {
        while(a[r+1].ts-a[i].ts<d&&r<y) 
		{
            r++;// 
		} 
		if(r-i+1>=k) {//帖子的数量不小于k 
            return 1;
		}
    }
    return 0;
}

int main() {
    cin >> n >> d >> k;
    for (int i = 1; i <= n; i++) 
	{
        cin >> a[i].ts >> a[i].id;
    }
    sort(a + 1, a + n + 1, cmp);//排序 
    
    for (int i = 1; i <= n; i++) 
	{
        int tmp = i;
        while (a[tmp + 1].id == a[i].id) 
		{
            ++tmp;
		}
        if (check(i, tmp)) 
		{
            cout << a[i].id << '\n';
        }
        i = tmp;//以便跳过已经处理过的相同编号的帖子 
    }
    return 0;
}

心得体会

1、这段代码定义了一个名为 node 的结构体,该结构体具有两个成员变量 ts 和 id,分别用于存储帖子的时间戳和编号。

struct node {
    int ts, id;
};

然后,声明了一个结构体数组 a,数组的大小为 100005,每个元素都是 node 类型的结构体。这个结构体数组 a 的作用是用来存储帖子的时间戳和编号。

在程序的其他部分,通过操作数组 a 的元素,可以获取和设置每个帖子的时间戳和编号。例如,a[0].ts 表示第一个帖子的时间戳,a[0].id 表示第一个帖子的编号。

通过定义这个结构体和结构体数组,可以方便地组织和存储帖子的相关信息,使得在后续的处理过程中能够更方便地访问和操作帖子的时间戳和编号。

2、在主函数部分的重点:在每次循环迭代开始时,定义一个整数变量 tmp,并将其初始化为当前的循环变量 i

while (a[tmp + 1].id == a[i].id) {
    ++tmp;
}

在每次循环迭代中用一个嵌套的 while 循环,不断增加变量 tmp 的值,直到遇到帖子编号不同于当前帖子编号 a[i].id 的帖子。目的是找到与当前帖子具有相同编号的连续帖子的最后一个帖子。

if (check(i, tmp)) {
    cout << a[i].id << '\n';
}

调用 check 函数,传入当前帖子的起始位置 i 和最后一个具有相同编号的帖子的位置 tmp,来检查这些帖子是否满足热帖的条件。如果满足条件,则输出当前帖子的编号 a[i].id

i = tmp; //以便跳过已经处理过的相同编号的帖子

最后,将循环变量 i 的值设置为 tmp,以便跳过已经处理过的具有相同编号的一组帖子。这样做是为了避免重复处理相同编号的帖子。

整个循环的目的是遍历帖子并对它们进行分组处理。对于每一组具有相同编号的连续帖子,找到最后一个帖子的位置,并调用 check 函数来检查这组帖子是否满足热帖的条件。如果满足条件,则输出该组帖子中的第一个帖子的编号。


[蓝桥杯 2018 省 B] 乘积最大

题目描述

给定 N 个整数A1​,A2​,⋯,AN​。请你从中选出K 个数,使其乘积最大。请你求出最大的乘积,由于乘积可能超出整型范围,你只需输出乘积除以1000000009(即10^9+9)的余数

注意,如果X<0, 我们定义 X 除以1000000009 的余数是0−((0−x)mod1000000009)。

标签:贪心算法

输入格式

第一行包含两个整数N 和K;以下 N 行每行一个整数Ai​。

输出格式

一个整数,表示答案。

代码表示

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mod 1000000009
int a[1000005];
signed main(){
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	} 
	sort(a+1,a+n+1);
//l是左指针,r是右指针,sum是记录已经处理的,ans乘积累积结果,f表示乘积的符号 
 	int l=1,r=n,sum=0,ans=1,f=1;
 	if(k%2==1)//k是奇数 
	{
 		ans*=a[n];//乘数组最大的数 
 		n--,k--,r--;
 		if(ans<0){
 			f=-1;
		}
	}
	k+=2;//由于循环一上来就要减去2所以这里先加上2 
 	while(k-=2)//循环条件是 k 大于等于 2
	{
		//li 表示左指针 l 和 l + 1 位置上两个元素的乘积,
		//ri 表示右指针 r 和 r - 1 位置上两个元素的乘积
 		int li=a[l]*a[l+1],ri=a[r]*a[r-1];
 		if(ri*f>=li*f)//比较左右指针的大小 
		{
 			r-=2;
 			ans*=ri%mod;
 			ans%=mod;
		}
		else{
			l+=2;
			ans*=li%mod;
			ans%=mod;
		}
		sum+=2;//记录已经处理的元素数量 
	}
	cout<<ans%mod;
}

心得体会

有负数的存在,我们思考负负得正,因为在迫不得已下不会正负相乘,所以可以先排序数组,然后从左右取两正或两负,这样一定乘出来的是个正数;

判断一下k的奇偶性,如果是奇数就先把最大的拿了,再判断一下最大的的正负就结束了。

代码的核心思想是:

  1. 首先,读取输入的整数 N 和 K,以及数组 a 的元素。

  2. 对数组 a 进行排序,使得数组中的元素按照非递减的顺序排列。

  3. 初始化左指针 l 为 1,右指针 r 为 n,计数器 sum 为 0,乘积结果 ans 为 1,符号标记 f 为 1。

  4. 如果 k 是奇数,将乘积结果 ans 乘以数组中最大的元素 a[n],并将 nk 和 r 分别减 1。如果乘积结果 ans 小于 0,则将符号标记 f 设置为 -1。

  5. 将 k 的值增加 2。

  6. 进入一个循环,循环的条件是 k 大于等于 2。在每次循环迭代中,计算左指针 l 和 l + 1 位置上的乘积 li,以及右指针 r 和 r - 1 位置上的乘积 ri

  7. 如果 ri * f 大于等于 li * f,则将右指针 r 减去 2,并将乘积结果 ans 乘以 ri,并对结果取模。

  8. 否则,将左指针 l 增加 2,并将乘积结果 ans 乘以 li,并对结果取模。

  9. 每次循环迭代后,将计数器 sum 增加 2,用于记录已经处理的元素数量。

  10. 循环结束后,输出乘积结果 ans 对 mod 取模的值。

总结:该代码通过贪心算法找到乘积的最大值。首先,选择一个特殊情况,将乘积结果乘以数组中最大的元素。然后,通过循环迭代,每次选择左指针和右指针位置上乘积较大的元素,更新乘积结果和指针位置。最后,输出乘积结果对 mod 取模的值。

贪心算法的关键:在于确定每一步的局部最优解。在每一步选择时,贪心算法不会回溯或重新考虑之前的选择,而是根据选择当前最优解的策略进行决策。因此,贪心算法不一定能够得到问题的全局最优解,但在许多情况下,它能够得到一个接近最优解的解答,并且具有高效性。


[蓝桥杯 2015 省 B] 生命之树

题目描述

在 X 森林里,上帝创建了生命之树。他给每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。上帝要在这棵树内选出一个节点集合S(允许为空集),使得对于S 中的任意两个点a,b,都存在一个点列 a,v1​,v2​,⋯,vk​,b 使得这个点列中的每个点都是S里面的元素,且序列中相邻两个点间有一条边相连。

在这个前提下,上帝要使得S 中的点所对应的整数的和尽量大。这个最大的和就是上帝给生命之树的评分。

经过 atm 的努力,他已经知道了上帝给每棵树上每个节点上的整数。但是由于 atm 不擅长计算,他不知道怎样有效的求评分。他需要你为他写一个程序来计算一棵树的分数。

输入格式

第一行一个整数n 表示这棵树有n 个节点。

第二行n个整数,依次表示每个节点的评分。

接下来n−1 行,每行 2个整数u,v,表示存在一条u到v的边。由于这是一棵树,所以是不存在环的。

输出格式

输出一行一个数,表示上帝给这棵树的分数。

代码表示

#include<bits/stdc++.h>
using namespace std;
int n;
int a[100005]; // 每个点的点权
long long dp[100005]; 
vector<int> adj[100005]; // 邻接表存储树的边关系

//u表示当前节点,fa表示当前节点的父节点。 
void dfs(int u, int fa) 
{
	dp[u] = a[u];//相当于初始化
	//遍历当前节点 u 的邻接节点 v;adj[u]存储了节点u所有邻接节点。 
	for(int v : adj[u]) 
	{
		if(v == fa) continue;
		dfs(v, u);
		dp[u] += max(dp[v], 0ll);
	}
   // 状态转移方程
}

int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) 
		scanf("%d", &a[i]);
	for(int i = 1, u, v; i < n; i++) {
		scanf("%d%d", &u, &v);
		adj[u].push_back(v);
		adj[v].push_back(u);
	}
	dfs(1, 0);
	//max_element函数找到dp数组中的最大值将其与0取最大值。 
	printf("%lld", max(*max_element(dp + 1, dp + n + 1), 0ll)); 
	return 0;
}	

心得体会

注意:若选择了某个节点,那么它的最大评分就是节点本身的评分➕其子节点选择的最大评分。若不选择该节点,那么它的最大评分就是其子节点选择的最大评分之和。我们需要在这两种情况中选择较大的评分作为当前节点的最大评分。

难难难/(ㄒoㄒ)/~~


[蓝桥杯 2019 省 B] 后缀表达式

题目描述

给定N 个加号、M 个减号以及N+M+1 个整数A1​,A2​,⋯,AN+M+1​,小明想知道在所有由这N 个加号、M 个减号以及N+M+1 个整数凑出的合法的后缀表达式中,结果最大的是哪一个。请你输出这个最大的结果。

例如使用 1 2 3 + -,则 2 3 + 1 - 这个后缀表达式结果是 4,是最大的。

输入格式

第一行包含两个整数N 和M。

第二行包含N+M+1个整数A1​,A2​,⋯,AN+M+1​。

输出格式

输出一个整数,代表答案。

代码表示

第一种 本人写的代码/(ㄒoㄒ)/~~只有30分哦,即不去考虑括号的问题!!

#include <bits/stdc++.h>
#define int long long//应该要开long long吧 
using namespace std;
int n,m,a[3000000],num1,num2;

int main() {
	cin>>n>>m;
	for(int i=1;i<=n+m+1;++i){
		cin>>a[i];
	}
	sort(a+1,a+n+m+2);//升序 

	for(int i=n+m+1;i>m;--i){
		num1+=a[i];
	}
	for(int j=1;j<=m;++j){
		num2+=a[j];
	}
    cout<<num1-num2;
	return 0;
}

第二种 因为在后缀表达式转中缀表达式时,很有可能会出现括号,而一旦出现括号,算是的过程就改变了,算出来的结果自然就是错的了,并且数据范围包括了负数,所以按以上方法是错误的。

该怎么处理呢:可以取绝对值;比如一个数转过去带上了括号,而且是个负数,所以要去负号,也就是求绝对值

#include <bits/stdc++.h>
#define int long long;//应该要开long long吧 
using namespace std;
int n,m,a[3000000],num,ans;
signed main(){
	cin>>n>>m;
	num=n+m+1;
	for(int i=1;i<=num;i++)cin>>a[i];
	sort(a+1,a+num+1); 
	if(m==0){//如果没有减号 
		for(int i=1;i<=num;i++)ans+=a[i];//无脑加 
		cout<<ans;//平平无奇的输出 
		return 0;
	}
	ans+=a[num];//先加最大的 
	ans-=a[1];//再减最小的 
	for(int i=2;i<num;i++) ans+=abs(a[i]);//无脑加绝对值 
	cout<<ans;//平平无奇的输出 
    return 0;
}

心得体会

1、在一个需要开long long 的程序中,我们在开头#define int long long 之后的 long long就都用int代替就OK。

2、排序数据中间个别元素,所以如果开头不是i=0的话还是按这个式子写!!

sort(arr + i, arr + j);那么被排序的就是arr[i] 到 arr[j - i]。


单词拆分·完全背包

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

思路提示

单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。

拆分时可以重复使用字典中的单词,说明就是一个完全背包!

动规五部曲分析如下:

1、确定dp数组以及下标的含义

dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

2、确定递推公式

如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true( j < i )。

所以递推公式是 if( [j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

3、dp数组如何初始化

从递推公式中可以看出dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。

dp[0]表示如果字符串为空的话,说明出现在字典里。

但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。

下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

4、确定遍历顺序

题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

本题其实是排列数,拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。

"apple", "pen" 是物品,那么我们要求物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。

"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。故本题一定是 先遍历背包,再遍历物品。

5、举例推导dp[i]

以输入: s = "leetcode", wordDict = ["leet", "code"]为例,dp状态如图:

代码表示

bool wordBreak(string s, vector<string>& wordDict) {
    unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
    //大小为 s.size() + 1,并初始化所有元素为 false 
    vector<bool> dp(s.size() + 1, false);
    dp[0] = true;//空字符串是可以被拆分
    for (int i = 1; i <= s.size(); i++) { // 遍历背包
        for (int j = 0; j < i; j++) {       // 遍历物品
            string word = s.substr(j, i-j); //substr(起始位置,截取的个数)
            //检查word是否在 wordSet中且前缀j对应的dp[j]是true 
            if (wordSet.find(word) != wordSet.end() && dp[j]) 
			{
                dp[i] = true;
                break;
            }
        }
    }
    return dp[s.size()];
}

背包问题总结:

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值