2025.5.24 JYU_GDCPC训练赛1(A、I、L、G、F题)
本次比赛以虚拟方式参与2025ICPC 国际大学生程序设计竞赛全国邀请赛(武汉)比赛链接
赛时情况:
1.先跑去做了A题【A题链接】

A题大意:
给定一个长度为n的数组a,及q次操作,每次操作让我们把第p个数字变成[l,r]范围内的数字k,若变成k,那么所需的时间增加|a[p]-k|,并且每次操作后不能与其他矛盾(即如果让你把a[p]变到[1,5],又让你变到[6,7],所有建议无法得到满足,输出-1)
A题做法:复杂度 O(n + q)
我们只要用一个PII数组,先初始化为-1,记录符合条件的区间,更新该区间的最大l和最小r,并判断是否符合要求,出现矛盾(l>r)直接标记,最后判断a[i]是离左区间近(l-a[i])、还是离右区间近(a[i]-r)、或者在区间内(0),或者不用修改(0),将1-n遍历一遍把各情况的值加起来就可以了
代码如下:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define endl '\n'
using namespace std;
const int N=110,M=2010;
int n,m,k;
int a[N];
struct Q{
int l,r;
}tr[N];
void solve(){
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>a[i];
memset(tr,-1,sizeof tr);
int l,r,p;
bool flag=0;
while(k--){
cin>>p>>l>>r;
if(tr[p].l==-1) tr[p].l=l,tr[p].r=r;//第一次更新区间
else {/第二次更新区间取最大l和最小r
int minr=min(r,tr[p].r);
int maxl=max(l,tr[p].l);
if(minr>=maxl) tr[p].l=maxl,tr[p].r=minr;
else flag=1;//新区间l>r了,注意这里不能直接输出,因为还要读入剩下的内容
}
}
if(flag) cout<<-1<<endl;
else{
int ans=0;
for(int i=1;i<=n;i++){
int l=tr[i].l,r=tr[i].r;
if(l==-1) continue;
if(a[i]<l||a[i]>r) ans+=min(abs(l-a[i]),abs(r-a[i]));
}
cout<<ans<<endl;
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--)solve();
}
2.然后就是I题了 I题链接

I题大意
“我们说一个整数x是这个网格的"宾果整数",如果至少满足以下两个条件之一:至少有一行,该行单元格中的所有整数都小于或等于 x;至少有一列,该列单元格中的所有整数都小于或等于 x 。”
构造题,给定一个正方形矩阵的长度n,和一个最小宾果整数k,我们要构造一个由整数1~n*n组成的矩阵,且符合一列或一行最大整数小于等于k,且其他行或列的宾果整数不能比k小,如果无法构造出来,则输出-1
I题做法
我们只需要将第一行构造成k,k-1,k-2,…,k-n+1,然后在位置(i,i)填上(k+i-1),其他位置按照剩下的数字顺序填完就可以了,这样第一行是满足宾果整数k的,位置(i,i)到位置(i,n)都会满足(k+i-1),位置(i,i)到位置(n,i)都会满足(k+i-1),即影响是呈十字架型的。最后我们要判断构造出来的数组会不会超限(k<n,第一排都无法满足最大小于k),(k+n-1>n*n,预先填入的数字超限,无法构造),最后按题目要求输出即可
//可惜赛时我们填的是(i,i-1)而不是(i,i),这样最后一列会出问题,WA了一发。从第2列开始填肯定没有问题,因为第一列的第一个数已经满足k了
代码如下:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define endl '\n'
using namespace std;
const int N=110,M=2010;
int n,m,k;
int a[55][55],st[3010];
struct Q{
int l,r;
}tr[N];
void solve(){
cin>>n>>k;
if(k<n||k+n-1>n*n){
cout<<"No"<<endl;
return;
}
memset(st,0,sizeof st);
memset(a,0,sizeof a);
int flag=0;
for(int i=1,j=k;i<=n;i++,j--){//填第一行
a[1][i]=j;
st[j]=1;
}
int idx=1;
for(int i=2;i<=n;i++){//填位置(i,i)
a[i][i]=k+i-1;
st[k+i-1]=1;
}
//将剩下的填完
for(int i=2;i<=n;i++)
for(int j=1;j<=n;j++)
if(a[i][j]==0){
while(st[idx])idx++;
a[i][j]=idx;
st[idx]=1;
}
cout<<"Yes"<<endl;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cout<<a[i][j]<<" ";
}
cout<<endl;
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--)solve();
}
3.接下来就是L题,这题我们卡了好久 L题链接

L题大意
“如果最小元素和最大元素的平均值等于中位数,我们说长度k的整数序列C=c1,c2,⋯,ck是good。长度k序列的中位数定义为序列中的第⌈k/2⌉个最小元素,其中⌈x⌉是大于或等于x的最小整数。”
“给定一个整数序列 A=a1,a2,⋯,an,计算其最长的good子序列的长度。回想一下,序列B是A的子序列,可以通过从中删除一些元素或没有元素来获得B,而无需更改其余元素的顺序。”
1.注意这里的中位数不是原序列正中间的数,而是排序后的第\(⌈k/2⌉\)个最小元素;
2.我们要求的子序列是可以不连续的,中位数也是排序后的,所以我们可以直接排序a数组;
3.对于每个中位数,它的两倍需要等于最小的数+最大的数
L题 做法 O(n^2)
为了找到最长的满足(中位数的两倍等于最小的数+最大的数)的子序列,我们可以先枚举每一个中位数,对于每一个中位数,我们可以初始化l=1,r=n,随后根据(中位数的两倍与最小的数+最大的数的关系,具体看代码)将区间缩小,找到第一次相等的地方,i-l即为中位数左边有多少个数,r-i为右边有多少个数,根据该题中位数的定义,我们可以根据左右数字个数分配最大长度,我们用nn来表示min(i-l,r-i),当左边的满足的数较多时,最大长度为2nn+1(左右等长,i在中间);当相等时,最大长度为2nn+1,(左右等长,i在中间);当右边的满足的数较多时,最大长度为2*nn+2(长度为偶数,i的左边有nn个数,右边有nn+1个数,中位数靠左),枚举完中位数后最大的长度即为ans
因为超时问题WA了5发,最后俞同学是我们的大功臣!
代码如下:
点击查看代码
#include <bits/stdc++.h>
//#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
using namespace std;
const int N=110,M=2010;
int n,m,k;
int a[3010],st[3010];
void solve(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n);
if(a[1]==a[n]){
cout<<n<<endl;
return;
}
int ans=1;
for(int i=1;i<=n;i++){//枚举每一个中位数
int l=1,r=n;//初始化为整个区间大小,随后将区间缩小
//a[l]+a[r]如果太大了,把右区间减小可以使得a[l]+a[r]变小;如果太小了,将左区间增大可以使得a[l]+a[r]变大,第一次相等就跳出
while(l<r){
if(a[l]+a[r]>2*a[i]) r--;
else if(a[l]+a[r]<2*a[i]) l++;
else break;
}
if(a[l]+a[r]==2*a[i]){
int nn=min(r-i,i-l);
if(i-l>=r-i) ans=max(ans,2*nn+1);
else ans=max(ans,2*nn+2);
}
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--)solve();
}
4.最后去写G题,可惜我们赛时不知道如何去重,只想到了容斥原理的方法,没想到也可以用dp递推来求。(根号分治)G题链接

G题大意
给定 n×m 的网格,每个格子有个颜色a[i,j]。考虑所有从左上角走到右下角的路径,每次只能往右或往下走,路径的价值是它经过的不同颜色种数。求所有路径价值之和。
G题做法
1.考虑贡献法:格子(i,j) 的贡献,是从 (1,1) 走到(i,j)的过程中,没有经过颜色a[i,j]的路径数。如果把同种颜色的格子看成障碍,就是从(1,1)走到(i,j)不经过障碍的路径数。
2.对于每个点的贡献,有两种计算方法:容斥做法:O(\(k^2\)) 其中k是障碍数;DP做法: O(n*m)
若只使用其中一种做法,极端情况下的复杂度都是O(\((n*m)^2\)),所以我们要根据数据范围进行根号分治
补题如下:
点击查看代码
#include <bits/stdc++.h>
#define PII pair<int,int>
#define int long long
#define LL long long
#define pb push_back
#define fi first
#define se second
#define endl '\n'
using namespace std;
const int N=1e5+10,M=1010,mod=998244353;
const int INF=0x3f3f3f3f;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,k;
int fact[N],infact[N];
int qmi(int a,int b){
int c=1;
while(b){
if(b&1) c=c*a%mod;
a=a*a%mod;
b>>=1;
}
return c;
}
void init(){
fact[0]=infact[0]=1;
for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
infact[N-1]=qmi(fact[N-1],mod-2);
for(int i=N-2;i>=1;i--) infact[i]=infact[i+1]*(i+1)%mod;
}
int C(int a, int b){
if(b<0||a<b) return 0;
return fact[a]*infact[b]%mod*infact[a-b]%mod;
}
bool cmp(PII a,PII b){
if(a.first!=b.first) return a.first<b.first;
return a.second<b.second;
}
int fun1(vector<PII>& a,int c){
//此时的a对应的是mp[c],mp[c]存储了值为c的点下标,len为该值的数量
int len=a.size();
vector<int> f(len,0);
//f[i]数组是来计算(1,1)到第i个点的不重复路径的数量
int res=0;
for(int i=0;i<len;i++){//设第i个该类型的点为(l,r),第j个为(ll,rr)
//C(l+r-2,l-1)为从(1,1)到(l,r)不管前面是否重复的路径总数
f[i]=C(a[i].fi+a[i].se-2,a[i].fi-1);
for(int j=0;j<i;j++){
//(ll<=l&&rr<=r)点j应该在点i的矩形内才会影响到该点
if(a[j].fi<=a[i].fi&&a[j].se<=a[i].se){
/*
公式分析:f[i]={f[i]-f[j]*C(l+r-ll-rr,l-ll)%mod+mod}%mod;
1. C(l+r-ll-rr,l-ll): 计算i点到j点有多少条路径
2. f[j]*C(l+r-ll-rr,l-ll):前半段路径数*后半段路径数即为(1,1)到(ll,rr)的路径数
3. f[i]-f[j]*C(l+r-ll-rr,l-ll): 用i点前的符合条件的j点来更新f[i],
最后获得的就是(1,1)到(l,r)的路径数量了
*/
f[i]=(f[i]-f[j]*C(a[i].fi+a[i].se-a[j].fi-a[j].se,a[i].fi-a[j].fi)%mod+mod)%mod;
}
}
//num=f[i]*C(n+m-i-j,n-i)%mod;与(公式分析2)同理,num为点(l,r)对(1,1)到(n,m)的路径数贡献
int num=f[i]*C(n+m-a[i].fi-a[i].se,n-a[i].fi)%mod;
res=(res+num)%mod;
}
return res;
}
int fun2(vector<vector<int>>& g,int c){
//dp来递推去掉所有值为c的点后的路径总数
vector<vector<int>> dp(n+10,vector<int>(m+10,0));
dp[1][1]=(g[1][1]==c)?0:1;
//数字三角形模型计算路线总数
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(g[i][j]==c) continue;//去掉该值为c的点
if(i==1&&j==1) continue;
//该位置的贡献是由它上面和左边递推得到的
dp[i][j]=(dp[i-1][j]+dp[i][j-1])%mod;
}
}
//(1,1)到(n,m)的路径总数 减去 去掉所有值为c的点后的路径总数 即为值为c的点对路径数的贡献
int res=(C(n+m-2,n-1)-dp[n][m]+mod)%mod;
return res;
}
void solve(){
cin>>n>>m;
vector<vector<int>> a(n+10,vector<int>(m+10,0));//原图
set<int> v;//不同整数的集合
map<int,vector<PII>> mp;//不同整数的集合 与相同种类整数的下标
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
v.insert(a[i][j]);
mp[a[i][j]].pb({i,j});
}
}
for(auto& it:v) sort(mp[it].begin(),mp[it].end(),cmp);//将各点按大小排序
int ans=0;
int mid=sqrt(n*m);//(根号分治)
for(auto it:v){
int x,len=mp[it].size();
if(len<=mid) x=fun1(mp[it],it);//当点数较少时用容斥原理来算
else x=fun2(a,it);//点数较多时用dp来递推去掉所有值为it的点后的路径总数
ans=(ans+x)%mod;//各点到终点的路径之和即为答案
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
init();
int _=1;
cin>>_;
while(_--) solve();
return 0;
}
5.赛时看了一眼F题,以为就是dp,没想到很好的方法,因为数据范围较大,感觉二分也不太可行,过的人也不多,就没有去想了,赛后看到师兄的方法发现也不会特别难写。所以说一般出不来题的时候不一定要硬嗑多人过的题(但是这么多人过肯定是有道理的,是数据弱了还是想复杂了?也有可能就是自己的问题//虽然一般情况下少人过也肯定不好写就是了

F题大意
F题链接
有n种物品,第i种有ai个,每个的重量是\(2^{bi}\)。现在把所有物品放进m个承重相同的背包,问背包的最小承重k。我们的目标是找到最小的k值,使得所有物品能够被放入这m个背包中,并且每个背包内的总重量不超过k。特别注意,每个物品必须放置到恰好一个背包中。
F题做法
贪心。从1e9到0考虑每个p,考虑所有重量大等于\(2^p\)的物品,并把每个物品拆成重量为\(2^p\)的若干份,然后动态维护剩余的可用容量。通过将剩余容量转换为不同重量单位下的等效值,避免直接模拟或二分查找。
定义当前剩余容量为 $ k $ (注意:这里的 $ k $ 并非原始的背包容量,而是通过不断转化得到的相对容量),对于每种重量为 $ 2^b $ 的物品,我们需要判断当前剩余容量是否能够容纳这类物品。具体步骤如下:
- 判断当前剩余容量:检查现有的相对容量k是否足以容纳所有重量为\(2^b\)的物品。
- 新增背包需求:如果当前剩余容量不足以容纳这些物品,则需要计算需新增多少个背包,并确定这些新增背包能提供的总容量。
- 容量转换:将新增背包所提供的总容量转化为下一个较小的重量单位(即 $ 2^{b_{2}}$),其中\(2^{b_{2}}< 2^b\))下的等效容量。这一步骤相当于将剩余的空间“压缩”到下一个更小的重量单位上。
点击查看代码
#include <bits/stdc++.h>
#define PII pair<int,int>
#define int long long
#define LL long long
#define lc (p<<1)
#define rc (p<<1|1)
#define lowbit(x) (x&-x)
#define endl "\n"
using namespace std;
const int N=1e6+10,M=1010,mod=998244353,INF=0x3f3f3f3f;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,k,kk,u,v;
int qpow(int a,int b){
int res=1;
a%=mod;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
void solve(){
cin >> n >> m;
map<int, int> mp; //mp[i]存储每种重量为2^i的物品总数
for (int i = 0; i < n; ++i) {
int a,b;
cin>>a>>b;
mp[b]+=a; // 合并相同重量为2^b的物品数量
}
vector<int> keys;//存储所有不同的b值
for (auto &it: mp)keys.push_back(it.first);
sort(keys.begin(), keys.end(), greater<int>());
if (keys.empty()) { //特判没有物品的情况
cout << "0\n";
return;
}
int ans = 0,k = 0,pre = keys[0];
// k为当前剩余容量,pre为上一个处理的 b 值
for (int b:keys) { //遍历每个不同的重量单位 b
if (k > 0) { // 如果还有剩余容量,则进行容量单位转换
int diff = pre - b; // 计算前后两个b值之间的差值
// 如果差值太大或转换后会溢出,则直接退出循环
if (diff >= 50 || k > (1LL << (50 - diff))) {
break;
}
// 将上一个单位下的容量转换为当前单位下的等效容量
k *= (1LL << diff);
}
pre = b; // 更新前一个 b 值
// 判断当前容量是否足够容纳该类物品
if (k >= mp[b]) {
k -= mp[b]; // 足够的话直接扣除已使用的容量
continue;
}
// 容量不足时,需要新增背包
mp[b] -= k,k=0; // 把可以放的当前重量为2^b的物品放进去,然后清空当前容量
// 计算所需背包数量(向上取整)
int x = (mp[b] + m - 1) / m;
// 累加贡献:x 个背包 × 2^b 的价值
ans = (ans + x % mod *qpow(2, b) % mod) % mod;
// 更新剩余容量,用于下一个更小的重量单位
k = x * m - mp[b];
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
while(t--) solve();
}
不经一翻彻骨寒,怎得梅花扑鼻香。 ——宋帆
浙公网安备 33010602011771号