CCF-CSP认证考试准备第十天


### Day10: 1.202206-2 2.202209-2(先放一下) 3.202212-2

#### 1.202206-2:寻宝!大冒险!(枚举,稀疏数组,犯了一个边界判断逻辑错误,只有60,改正即为100) 
(1)一开始漏了绿化图是0但宝藏图是1的这种情况,但加上去只有60分
思路:宝藏图很大,只要存储几个为1的点的坐标即可,有点类似稀疏向量,用set存储即可快速判断一个坐标在宝藏图中是否为1(哈希思想),原来也打算用另一个set存储绿化图,但这样会存在漏了绿化图是0但宝藏图是1的这种情况,考虑绿化图不大,直接用二维数组存储即可
60分代码:
```
#include <bits/stdc++.h>

using namespace std;

int main(){
    ios::sync_with_stdio(false);
    int n,L,S;
    cin>>n>>L>>S;
    set<pair<int,int>> sTreeMap;
    int x,y;
    for(int i=0;i<n;i++){
        cin>>x>>y;
        sTreeMap.insert({x,y});
    }
    vector<vector<int>> vMap(S+1,vector<int>(S+1,0));
    for(int i=S;i>=0;i--){
        for(int j=0;j<=S;j++){
            cin>>vMap[i][j];
        }
    }
    int res=0;
    for(auto it=sTreeMap.begin();it!=sTreeMap.end();it++){
        int offx=it->first;
        int offy=it->second;
        if(offx+S>L && offy+S>L){
            continue;
        }
        bool isMap=true;
        for(int i=0;i<=S;i++){
            for(int j=0;j<=S;j++){
                x=i+offx;
                y=j+offy;
                if(vMap[i][j]==1){                            
                    if(sTreeMap.find({x,y})==sTreeMap.end()){//绿化图是1但宝藏图是0 
                        isMap=false;
                        break;
                    }
                }
                else{
                    if(sTreeMap.find({x,y})!=sTreeMap.end()){//绿化图是0但宝藏图是1 
                        isMap=false;
                        break;
                    }
                }
            }
            if(!isMap){
                break;
            }
        }
        if(isMap){
            res++;
        }
    }
    cout<<res;
    return 0;
}
```
(2)60分原因:
**边界判断**:
if(offx+S>L && offy+S>L){
    continue;
}
&&是错误的,应该为||,就是满分(**重点注意**)
(3)优化学习:
用map<pair<int,int>,int>来存储宝藏图,pair为坐标,最后一个int为值0或1,直接比较vMap[i]  [j]  是否等于mp[{x,y}]即可
代码:
```
#include <bits/stdc++.h>

using namespace std;

int main(){
    ios::sync_with_stdio(false);
    int n,L,S;
    cin>>n>>L>>S;
    map<pair<int,int>,int> mpTreeMap;
    vector<vector<int>> vMap(S+1,vector<int>(S+1,0));
    int x,y;
    for(int i=0;i<n;i++){
        cin>>x>>y;
        mpTreeMap[{x,y}]=1;
    }
    for(int i=S;i>=0;i--){
        for(int j=0;j<=S;j++){
            cin>>vMap[i][j];
        }
    }
    int res=0;
    for(auto ele:mpTreeMap){
        int offx=ele.first.first;
        int offy=ele.first.second;
        if(offx+S>L || offy+S>L){
            continue;
        }
        bool isMap=true;
        for(int i=0;i<=S;i++){
            for(int j=0;j<=S;j++){
                x=i+offx;
                y=j+offy;
                if(vMap[i][j]!=mpTreeMap[{x,y}]){
                    isMap=false;
                    break;
                }
            }
            if(!isMap){
                break;
            }
        }
        if(isMap){
            res++;
        }
    }
    cout<<res;
    return 0;
}
```

#### 2.202209-2:何以包邮?(DP,01背包问题)
(1)枚举70分:
思路:
**子集枚举**:
- 通过 `for (int i = 0; i < (1 << n); ++i)` 枚举所有子集。`(1 << n)` 表示 `2^n`,即总共有 `2^n` 种子集(数学计算得出)。
- 对于每个子集,使用 `i & (1 << j)`(**按位与操作,不是&&**) 判断第 `j` 本书是否在该子集中。如果在,则将其价格加入 `sum` 中。
**解释**:
- 当我们从 0 迭代到 2^n - 1 时,我们实际上遍历了从 `000...0` 到 `111...1` 的所有 n 位二进制数。
- 每个 n 位二进制数可以看作一个子集的表示:
    - 如果第 k 位(从右到左数)是 1,则表示该子集包含第 k 本书。
    - 如果第 k 位是 0,则表示该子集不包含第 k 本书。
**例子**: 假设 i = 5(二进制 `101`),我们想检查第 1 本书是否在子集中。
- `1 << 1` 得到 `0010`。
- 然后,`101` & `0010` 结果为 `0000`,即第 1 本书不在子集中。
**再举一个例子**,假设 i = 6(二进制 `110`),我们想检查第 2 本书是否在子集中。
- `1 << 2` 得到 `0100`。
- 然后,`110` & `0100` 结果为 `0100`,即第 2 本书在子集中。
```
#include <bits/stdc++.h>

using namespace std;

int main(){
    ios::sync_with_stdio(false);
    int n,x;
    cin>>n>>x;
    int minCost=INT_MAX;
    vector<int> v(n,0);
    for(int i=0;i<n;i++){
        cin>>v[i];
    }
    for(int i=0;i<(1<<n);i++){//i<2^n,i表示集合 
        int sum=0;
        for(int j=0;j<n;j++){
            if(i & (1<<j)){//i的j位是否为1(按位与操作),即j本书是否在此次i集合内 
                sum+=v[j];
            }
        }
        if(sum>=x){
            minCost=min(sum,minCost);
        }
    }
    cout<<minCost;
    return 0;
}
```
(2)01背包DP100分(**重点学习优化的两种思路**):
**学习**:
(1)01背包基础:
[[DP#1.01背包:]]
目的:每种水果都有对应的两种属性:占用的体积V和蕴含的价值W。而你的背包体积为N。老板说:每种水果只能拿一个!因此对于咱们肯定得想一种搭配方式使得**拿的水果总体积不超过背包容积,但是价值总和达到最大!**
**核心: 
f[i]  [j]:表示所有选法集合中,只从前i个物品中选,并且总体积不大于j的选法的集合,它的值是这个集合中每一个选法的最大值。
对于01背包问题选择方法的集合可以分成2种:
1.不选第i个物品,并且总体积不大于j的集合所达到的最大值:f[i-1]  [j]
2.选择1~i个物品,并且总体积不大于j的集合所达到的最大值f[i]  [j]
对于第二种情况我们很难计算,因此需要思考从另一个角度解决问题。(==重点==)当选择1~i个物品,总体积不大于j的集合的最大值可以转化成选择1~i-1个物品,总体积不大于j-V[i]的集合+最后一个物品的价值:f[i-1]  [j-V[i]]+w[i]
**重点**:因此总结:f[i]  [j]= Max{f[i-1]  [j],f[i-1]  [j-v[i]]+w[i]}
(2)应用到本题:
#### 法一:
**不存在背包容量这个概念,只有背包价值,或者理解为背包容量和背包价值都是书本价格(因为限制条件和寻求最优解的都是书本价格)**
书本的总价格>=x并且 最小  
**核心转换**:可以将问题理解为 **选取几本书(所有书中题目所选的书剩下的书)使得书本的总价格<= sum - x的情况下,使得书本的总价格最大**(01背包),所要求的花费最小值即为sum-dp里面的最大值
**`dp[i][j]`**:在前 `i` 本书中选择若干本书,总价格为 `j` 元时能够得到的最大价格。
**dp核心部分**:
```
for(int i=1;i<=n;i++){//i表示书籍索引
        for(int j=0;j<=sum-x;j++){//j表示最大价格
            if(j<vPrices[i]){//第i本书无法被选择
                dp[i][j]=dp[i-1][j];//继承前i-1本书的最大价格状态
            }
            else{//第i本书可以被选择
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-vPrices[i]]+vPrices[i]);//比较不选择第i本书的最大价格和选择第i本书的最大价格
            }
            maxCost=max(maxCost,dp[i][j]);
        }
    }
```

 ![[01背包1.png]] **代码**:
 ```
 #include <bits/stdc++.h>

using namespace std;

const int N=35;
const int M=300010;
int dp[N][M];

int main(){
    ios::sync_with_stdio(false);
    int n,x;
    cin>>n>>x;
    int sum=0;
    vector<int> vPrices(n+1,0);
    for(int i=1;i<=n;i++){
        cin>>vPrices[i];
        sum+=vPrices[i];
    }
    int maxCost=-1;
    for(int i=1;i<=n;i++){
        for(int j=0;j<=sum-x;j++){
            if(j<vPrices[i]){
                dp[i][j]=dp[i-1][j];
            }
            else{
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-vPrices[i]]+vPrices[i]);
            }
            maxCost=max(maxCost,dp[i][j]);
        }
    }
    cout<<sum-maxCost;
    return 0;
}
 ```
#### **法一优化**(反着求,求补集):
将二维数组简化为一维数组(如果书本数量太多能大大优化)
在0/1背包问题中,`dp[i][j]` 仅依赖于 `dp[i-1][j]` 和 `dp[i-1][j-重量]` 的值。因此,我们可以通过逆序更新 `dp[j]`,在保证每个物品只被考虑一次的同时,使用一维数组来保存结果。
**`dp[j]` 表示:在前 `i` 本书中,选择若干本书,使得总花费恰好为 `j` 时的最大总价值。**
**重点**:
**逆序遍历的原因**:逆序遍历是为了保证在更新 `dp[j]` 时,`dp[j - vPrices[i]]` 仍然代表的是没有选择第 `i` 本书时的状态(因为这时还没有更新`dp[j - vPrices[i]]`,所以肯定不包含第i本书)。如果我们正序遍历,`dp[j - vPrices[i]]` 可能已经包含了第 `i` 本书(例如:第i本书价格为2,
 更新 `dp[2]` 时,`dp[2] = max(dp[2], dp[2 - 2] + 2) = max(0, 0 + 2) = 2
更新 `dp[4]` 时,`dp[4] = max(dp[4], dp[4 - 2] + 2) = max(0, 2 + 2) = 4`(错误,这里实际上用了当前物品两次))
这样会导致每本书被多次考虑,破坏了0/1背包的约束条件。
**代码**:
```
vector<int> dp(sum+1,0);//求最大值就全赋较小的值
for(int i=1;i<=n;i++){
    for(int j=sum;j>=vPrices[i];j--){//逆序遍历
        dp[j]=max(dp[j],dp[j-vPrices[i]]+vPrices[i]);
    }
}
cout<<sum-dp[sum-x];//输出要注意
```
**注意**:这种dp求最大值所得的dp[j]就是总花费恰好为 `j` 时的最大总价值,所以直接dp[sum-x]就是要求的最大值,与下面的解法二优化区分开来
#### 法二(较难理解):
整个问题也可以看作是01背包问题  
**从前i本书中选择要被减掉的书,使得总价格>= x的所有选法**  
求所有选法集合中的价格最小值即是答案
![[01背包2.png]]

**`f[i][j]` 的含义**:`f[i][j]` 是动态规划状态,表示在考虑前 `i` 本书时,总花费为 `j` 元时的最小可能花费。
**注意**:因为求最小值,要提前初始化为很大的值
**代码**:
```
#include <bits/stdc++.h>

using namespace std;

const int N=35;
const int M=300010;
int dp[N][M];

int main(){
    ios::sync_with_stdio(false);
    int n,x;
    cin>>n>>x;
    vector<int> vPrices(n+1,0);
    int sum=0;
    for(int i=1;i<=n;i++){
        cin>>vPrices[i];
        sum+=vPrices[i];
    }
    for(int i=1;i<=n;i++){
        dp[i][sum]=sum;
    }
    for(int j=x;j<=sum;j++){
        dp[0][j]=sum;
    }
    int minCost=INT_MAX;
    for(int i=1;i<=n;i++){
        for(int j=sum;j>0;j--){
            if(j+vPrices[i]<=sum){
                dp[i][j]=min(dp[i-1][j],dp[i-1][j+vPrices[i]]-vPrices[i]);
            }
            else{
                dp[i][j]=dp[i-1][j];
            }
            if(dp[i][j]>=x){
                minCost=min(minCost,dp[i][j]);
            }
        }
    }
    cout<<minCost;
    return 0;
}
```
#### **法二优化**(正着求):
**`dp[j]` 表示:在前 `i` 本书中,选择若干本书,使得总花费恰好为 `j` 时的最小总花费。**(是恰好等于,而不是大于等于)
```
#include <bits/stdc++.h>

using namespace std;

const int M = 300010;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, x;
    cin >> n >> x;

    vector<int> vPrices(n + 1, 0);
    int sum = 0;

    for (int i = 1; i <= n; i++) {
        cin >> vPrices[i];
        sum += vPrices[i];
    }

    vector<int> dp(sum + 1, M);//求最小值就全赋较大的值
    dp[0] = 0;//特殊1

    for (int i = 1; i <= n; i++) {
        for (int j = sum; j >= vPrices[i]; j--) {
            dp[j] = min(dp[j], dp[j - vPrices[i]] + vPrices[i]);
        }
    }
    //特殊2
    int res = M;
    for (int j = x; j <= sum; j++) {
        res = min(res, dp[j]);
    }

    cout << res << endl;//直接输出
    return 0;
}

```
**注意**:
这种DP方法记录总花费恰好为 `j` 时的最小总花费所得的dp[j]的值要么等于初始值M,要么等于j(因为dp[0]=0),所以最后输出要遍历x到sum找到最小值输出,即为大于等于x,否则输出dp[x]为恰好为x元的最小价值,如果不存在恰好等于,那么dp[x]就为初始化的一个极大的值,错误
与上面的解法一优化区分开来
#### 3.202212-2:训练计划()
(1)一开始只有输出最早时间测试70分,之后加上输出最晚时间还是70分,原因如下:
每项科目最多只依赖一项别的科目,且满足依赖科目的编号小于自己,这句话不能保证一个被依赖的项目所依赖的项目只有一个,可以有多个,我用结构体int类型int backRelyIndex记录反向被依赖索引会出现覆盖问题,产生错误,可以换成数组(**单一的线性依赖关系变成复杂的依赖关系**),以及**注意计算最晚开始时间是后一个依赖索引的最晚结束时间减去前一个被依赖索引的天数**
(2)代码(100):
```
#include <bits/stdc++.h>

using namespace std;

struct Pro{
    int relyIndex;//依赖索引 
    vector<int> backRelyIndex;//反依赖索引(可以有多个,如样例2) 
    int day;
    int first;
    int end;
};

int main(){
    ios::sync_with_stdio(false);
    int n,m;
    cin>>n>>m;
    vector<Pro> v(m+1);
    for(int i=1;i<=m;i++){
        cin>>v[i].relyIndex;
        if(v[i].relyIndex!=0){
            v[v[i].relyIndex].backRelyIndex.push_back(i);
        }    
    }
    for(int i=1;i<=m;i++){
        cin>>v[i].day;    
    }
    //计算最早开始天数 
    int maxlen=-1;
    for(int i=1;i<=m;i++){
        if(v[i].relyIndex==0){
            v[i].first=1;
        }
        else{
            v[i].first=v[v[i].relyIndex].first+v[v[i].relyIndex].day;
        }
        maxlen=max(maxlen,v[i].first+v[i].day-1);
    }
    for(int i=1;i<=m;i++){
        cout<<v[i].first<<" ";
    }
    //计算最晚天数 
    if(maxlen<=n){
        cout<<endl;
        for(int i=m;i>0;i--){
            if(v[i].backRelyIndex.empty()){//没有反依赖的 
                v[i].end=n-v[i].day+1;
            }
            else{
                int minEnd=INT_MAX;//找最小的end才能满足所有 
                for(auto x:v[i].backRelyIndex){
                    minEnd=min(minEnd,v[x].end-v[i].day);//注意这边倒推的计算,是最晚开始时间是后一个依赖索引的最晚结束时间减去前一个被依赖索引的天数,且不需要加1 
                }
                v[i].end=minEnd;
            }                                                
        }
        for(int i=1;i<=m;i++){
            cout<<v[i].end<<" ";
        }
    }
    return 0;
}
```
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值