2025“钉耙编程”中国大学生算法设计暑期联赛(6)01/04/08/09

个人做题顺序/大致难度排序

1. 1009 对撞器

知识点:签到

如果最大值在两端,则可以让这个最大值直接产生 \(n-2\) 次能量

如果最大值在中间,则这个最大值可以产生 \(n-3\) 次能量,然后 \(a[1],a[n]\) 再碰撞一次产生能量

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n;
    cin>>n;

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

    if(n<=2){
        cout<<0<<endl;
        return;
    }

    int idx=0,mx=0;
    for(int i=1;i<=n;i++){
        if(a[i]>mx){
            mx=a[i];
            idx=i;
        }
    }

    int ans=0;

    if(idx!=1 && idx!=n) ans=max(a[1],a[n])+(n-3)*mx;
    else ans=mx*(n-2);

    cout<<ans<<endl;
}   

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

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

2. 1001 cats 学乘法

知识点:签到

分情况讨论

  1. 如果原序列不存在 \(0\)
    1. 如果负数个数是偶数,则答案为 \(0\)
    2. 如果负数个数是奇数,则需要把一个负数变成正数,或把一个正数变成正数。二者取最小值即为答案
  2. 如果原序列存在 \(0\), 还是考虑上面两种情况,但是直接输出 \(0\) 的个数即可
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
	int n;
    cin>>n;
    int c0=0,neg=0,pos=0;//分别表示0,负数,正数的个数
    int mxneg=-inf,mnpos=inf;//最大的负数,最小的正数

    for(int i=1;i<=n;i++){
        int val;
        cin>>val;
        if(val==0) c0++;
        else if(val<0){
            neg++;
            mxneg=max(mxneg,val);
        }
        else{
            pos++;
            mnpos=min(mnpos,val);
        }
    }

    //没有0
    if(c0==0){
        if(neg&1){
            //负数有奇数个
            cout<<min(mnpos,-mxneg)+1<<endl;
        }
        else{
            //负数有偶数个
            cout<<0<<endl;
        }
    }
    else{//有0存在
        cout<<c0<<endl;
    }
}

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

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

3. 1004 传送排序

知识点:动态规划,数据结构维护dp,线段树/树状数组

首先对答案的大概想法是,有一些猫猫不移动,有一些猫猫要移动,而不移动的猫猫一定是一个递增的子序列

但是仔细想想好像又不能确定使用哪个子序列,因为还要考虑防止传送门

考虑 DP 做法

设不移动的猫猫为集合 \(U\),集合的大小为 \(|U|\), 对这个集合 \(U\) 需要放置 \(B(U)\) 个传送门

则最少操作次数为:\(n~-~|U|~+~B(U)\)

则可以定义不移动的猫猫集合 \(U\)的得分为:\(|U|~-~B(U)\),我们要让这个得分最大化

\(dp[i]\) 表示:以编号 \(i\) 的猫猫为递增子序列的最后一只猫猫,的得分最大值

注意这里的 \(i\) 是表示猫猫编号(数值)而不是下标

则状态转移有三种方式

  1. \(i\) 作为集合 \(U\) 中的唯一元素,\(|U|=1\),此时如果 \(i\)\(1\)\(n\),则 \(B(U)=1\) ;否则\(B(U)=2\)

    此时 \(dp[i]=1- B(U)\)

  2. \(i\) 作为集合 \(U\) 中的最后一个元素,且上一个元素 \(j=i-1\),此时 \(dp[i]=dp[j]+1+(i==n)\)

    其中 \(1\) 表示元素 \(i\)\((i==n)\) 表示此时可以节省一个传送门

    注意这里 \(j\) 的位置必须位于 \(i\) 前面

  3. \(i\) 作为集合 \(U\) 中的最后一个元素,且上一个元素 \(j<i-1\),此时 \(dp[i]=dp[j]+1-(i!=n)\)

    其中 \(+1\) 代表元素 \(i\)\(-(i!=n)\) 表示如果 \((i!=n)\) 则需要在 \(i\) 后面放一个传送门

    注意这里 \(j\) 的位置必须位于 \(i\) 前面。

    可以用线段树或树状数组维护所有 \(dp[1]\)\(dp[j-2]\) 的最大值

最后四种方式取最大值即可,注意枚举完 \(i\) 后线段树插入的是 \(dp[i-1]\) (因为转移方式三要求 \(j<i-1\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;



class SegmentTree{
public:
    #define lc u<<1
    #define rc u<<1|1

    struct Node{
        int l, r;
        ll mx;
    };

    int n;
    vector<Node> tr;

    SegmentTree(int n){
        init(n);
    }

    void init(int n){
        this->n=n;
        tr.resize(4*n+10);
    }

    void pushup(int u){
        tr[u].mx = max(tr[lc].mx, tr[rc].mx);
    }

    void build(int u,int l,int r){
        tr[u] = {l, r, -inf};
        if (l == r) return;
        int mid = l + r >> 1;
        build(lc, l, mid);
        build(rc, mid + 1, r);
    }

    
    ll query(int u, int l, int r){
        if (l <= tr[u].l && r >= tr[u].r) return tr[u].mx;
        
        ll ans = -inf;
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) ans = max(ans, query(lc, l, r));
        if (r > mid) ans = max(ans, query(rc, l, r));
        return ans;
    }

    
    void modify(int u, int pos, ll val){
        if (tr[u].l == tr[u].r) {
            tr[u].mx = max(tr[u].mx, val);
            return;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (pos <= mid) modify(lc, pos, val);
        else modify(rc, pos, val);
        pushup(u);
    }
};


void solve(){
    int n;
    cin >> n;

    vector<int> a(n+1);
    vector<int> pos(n+1);
    bool f=1;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        pos[a[i]]=i;
        if(a[i]!=i){
            f=0;
        }
    }
    
    if(f){
        cout<<0<<endl;
        return;
    }

    SegmentTree seg(n);
    seg.build(1,1,n);

    vector<int> dp(n+1,0);
    ll mx=-inf;

    for(int i=1;i<=n;i++){
        // 1. U={i}
        int val=1-(i!=1)-(i!=n);
        // 2.连续扩展:从i-1扩展
        if(i>1 && pos[i-1]<pos[i]){
            val=max(val,dp[i-1]+1+(i==n));
        }
        // 3.非连续扩展:从j<i-1扩展
        if(pos[i]>1){
            val=max(val,seg.query(1,1,pos[i]-1)+1-(i!=n));
        }

        dp[i]=val;
        mx=max(mx,dp[i]);

        if(i>1){
            seg.modify(1,pos[i-1],dp[i-1]);
        }
    }
    
    int ans=n-mx;
    cout<<ans<<endl;
}   

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

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

4. 1008 cats 的 max

知识点:动态规划,状态压缩

我们来模拟从零开始做这道题的整个思考过程

1. 初看题面:目标是啥?

题目目标:

  • 给你一个 \(n×m\) 的矩阵 \(a\),你要从 \(n\) 行中选出 \(k\) 行。
  • 你选了 \(k\) 行之后,对于每一列 \(j\),拿这 \(k\) 行中这一列的最大值作为贡献。
  • 最终答案是这 \(m\) 个最大值的总和,问:选哪 \(k\) 行能让总和最大。

我们要从所有\(C(n,k)\) 种选择中,选出最优。

2. 第一反应:暴力能不能过?

试图暴力组合所有选行方案,复杂度:

\(C(n,k) * m=O(n^{k}*m)\)

这个复杂度根本跑不动。

直接暴力选 \(k\) 行不可行。

3. 想简化:能不能不枚举行,而枚举列?

注意贡献是每一列的最大值
也就是说,只要我们知道某几行中,第 \(j\) 列的最大值是多少,我们就能计算这个方案的总得分。

从这里我们意识到——

最终的评分是由每一列的最大值构成的

于是我们转换角度,假设我们“挑出来的行”,可以“分别负责”每一列的最大值。

4. 一个关键观察:如果 \(k≥m\),就可以直接做

因为每一列都可以由不同的行来“负责”,我们可以贪心地:

  • 对每一列 \(j\),从 \(n\) 行中取出最大值。

这是一个很重要的分支,也是我们真正建模的分界点

5. 剩下的情况:\(k<m\)

这时候我们没法“每列都选一行”了,行数不够用。

此时目标是:

\(n\) 行中选 \(k\) 行,组合在一起后,每一列从中取最大值,求总和最大。

此时我们需要真正去建模这个过程。

6. 建模 —— 观察小数据特性

注意到 \(m≤13\),我们脑海里该有个警钟响了:

有状态压缩的可能!

具体来说:

  • 所有的“列集合”一共只有 \(2^{13}=8192\) 个。
  • 我们可以尝试用二进制掩码来表示「被覆盖的列」。

7. 思考 DP 的可能建模方式

假设你已经选了几行,那么你当前这些行一共覆盖了哪些列?我们可以记录成一个掩码 mask

  • 于是我们可以定义:

    \(f[i][mask]\) 表示选了 \(i\) 行,覆盖了列 \(mask\) 的最大值

这个 DP 的目标是:

  • 枚举所有覆盖方式,用 \(k\) 行覆盖全集 \([0..m−1]\),取最大权值。

  • 最终答案是:

    \(f[k][(1<<m)-1]\)

8. 如何实现 DP 转移?

你已经选了 \(i−1\) 行,当前覆盖的是 mask
现在想选第 \(i\) 行,进一步覆盖 newMask = mask | submask

这个“新增的贡献”来自哪?

  • 来自某一行在子集 submask 上的和最大。

  • 所以我们预处理:

    \(val[submask]\) 所有 n 行中在子集 \(submask\) 上的最大和

然后转移:

for each i = 1 to k
    for each mask (已覆盖)
        for each submask of (~mask)
            f[i][mask | submask] = max(f[i][mask | submask], f[i-1][mask] + val[submask])

实现代码:

vector<vector<int>> f(k+1,vector<int>(1<<m,-inf));
f[0][0]=0;
//f[i][j]表示选了i行(i个集合,因为这里行和集合等价),且覆盖子集为j的最大值
//状态转移:
//将j分为两部分,a,b
//我们从前i-1行选a,则必须从第i行选b
//二者相加即可
for(int i=1;i<=k;i++){
    for(int mask=0;mask<(1<<m);mask++){
        int t=((1<<m)-1)^mask;
        //枚举mask补集t的所有子集,和mask合并
        for(int s=t;s;s=(s-1)&t){
            f[i][mask|s]=max(f[i][mask|s],f[i-1][mask]+val[s]);
        }
    }
    }

总结步骤

  1. 观察目标是按列取最大值,推导出可贪心处理的 \(k≥m\) 情况。
  2. 注意到 m 小,状态压缩建模,尝试用掩码来描述“已覆盖列集合”。
  3. 定义 DP 状态\(f[i][mask]\) 表示选 \(i\) 行,覆盖 \(mask\) 的最大值。
  4. 预处理 \(val[mask]\):每个子集 \(mask\) 的最大一行贡献。
  5. 状态转移:选新的一行,补上一些新列(子集),更新覆盖集。
  6. 最终目标\(f[k][(1<<m)-1]\),即用 \(k\) 行覆盖所有列的最优解。

你要做的,就是每到这一步,都去问自己:

  • 我是否有办法用小规模(比如 \(m\)) 来压住大的枚举空间(比如 \(n\))?
  • 我能不能反过来从“列”的角度看问题?
  • 有没有天然分界(如 \(k≥m\)) 让我先处理一部分?

看完题解后,我们需要处理出一个 \(val\) 数组,表示 \(1\) ~ \(n\) 行中,行内掩码为 \(mask\) 的值的最大值

暴力做法复杂度是 \(O(n*m*(1<<m)\),代码:

vector<int> val(1<<m);
for(int mask=0;mask<(1<<m);mask++){//注意这里先枚举mask后枚举n就可以过,但先枚举n再枚举mask就会TLE,原因是缓存命中率的问题
    for(int i=0;i<n;i++){
        int tmp=0;
        for(int j=0;j<m;j++){
            if((mask>>j) & 1){
                tmp+=a[i][j];
            }
        }
        val[mask]=max(val[mask],tmp);
    }
}

可以优化掉内层对 \(m\) 的循环,让第 \(i\) 行的 \(f[i][mask]\) 从更小的 \(mask\) 递推过来即可。

\(mask\) 可以从 \(lowbit(mask)\)\(mask-lowbit(mask)\) 递推过来,代码:

//val[mask]:1~n行中,行内掩码为mask的值的最大值
for(int i=0;i<n;i++){
    tval[0]=0;
    for(int mask=1;mask<(1<<m);mask++){
        int lbt=mask&(-mask);
        tval[mask]=tval[mask^lbt]+a[i][idx[lbt]];
        val[mask]=max(val[mask],tval[mask]);
    }
}

完整代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n,m,k;
    cin>>n>>m>>k;

    int a[n][m];
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            cin>>a[i][j];
        }
    }

    if(k>=m){
        int ans=0;
        for(int j=0;j<m;j++){
            int mx=0;
            for(int i=0;i<n;i++){
                mx=max(mx,a[i][j]);
            }
            ans+=mx;
        }
        cout<<ans<<endl;
        return;
    }

    //k<m
    vector<int> val(1<<m),tval(1<<m);
    vector<int> idx(1<<m);
    for(int i=0;i<m;i++){
        idx[1<<i]=i;
    }

    //val[mask]:1~n行中,行内掩码为mask的值的最大值
    for(int i=0;i<n;i++){
        tval[0]=0;
        for(int mask=1;mask<(1<<m);mask++){
            int lbt=mask&(-mask);
            tval[mask]=tval[mask^lbt]+a[i][idx[lbt]];
            val[mask]=max(val[mask],tval[mask]);
        }
    }

    vector<vector<int>> f(k+1,vector<int>(1<<m,-inf));
    f[0][0]=0;
    //f[i][j]表示选了i行(i个集合,因为这里行和集合等价),且覆盖子集为j的最大值
    //状态转移:
    //将j分为两部分,a,b
    //我们从前i-1行选a,则必须从第i行选b
    //二者相加即可
    for(int i=1;i<=k;i++){
        for(int mask=0;mask<(1<<m);mask++){
            int t=((1<<m)-1)^mask;
            //枚举mask补集t的所有子集,和mask合并
            for(int s=t;s;s=(s-1)&t){
                f[i][mask|s]=max(f[i][mask|s],f[i-1][mask]+val[s]);
            }
        }
    }
    cout<<f[k][(1<<m)-1]<<endl;
}

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

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}
posted @ 2025-08-06 01:16  LYET  阅读(151)  评论(0)    收藏  举报