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 学乘法
知识点:签到
分情况讨论
- 如果原序列不存在 \(0\)
- 如果负数个数是偶数,则答案为 \(0\)
- 如果负数个数是奇数,则需要把一个负数变成正数,或把一个正数变成正数。二者取最小值即为答案
- 如果原序列存在 \(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\) 是表示猫猫编号(数值)而不是下标
则状态转移有三种方式
-
\(i\) 作为集合 \(U\) 中的唯一元素,\(|U|=1\),此时如果 \(i\) 是 \(1\) 或 \(n\),则 \(B(U)=1\) ;否则\(B(U)=2\)。
此时 \(dp[i]=1- B(U)\)
-
\(i\) 作为集合 \(U\) 中的最后一个元素,且上一个元素 \(j=i-1\),此时 \(dp[i]=dp[j]+1+(i==n)\)。
其中 \(1\) 表示元素 \(i\),\((i==n)\) 表示此时可以节省一个传送门
注意这里 \(j\) 的位置必须位于 \(i\) 前面
-
\(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]);
}
}
}
总结步骤
- 观察目标是按列取最大值,推导出可贪心处理的 \(k≥m\) 情况。
- 注意到 m 小,状态压缩建模,尝试用掩码来描述“已覆盖列集合”。
- 定义 DP 状态:\(f[i][mask]\) 表示选 \(i\) 行,覆盖 \(mask\) 的最大值。
- 预处理 \(val[mask]\):每个子集 \(mask\) 的最大一行贡献。
- 状态转移:选新的一行,补上一些新列(子集),更新覆盖集。
- 最终目标:\(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;
}

浙公网安备 33010602011771号