【2027算法进阶班】NOIP训练赛#16 CSP_S信心赛(AK Vs 挂分)

难题

题目描述

奶牛给约翰出了一道难题,约翰来求助于你:

定义 \(f(i)\) 为非 \(i\) 因子的最小正整数,现给出正整数 \(n\) ,求 \(\sum_{i = 1}^{n}f(i)\) 对 \(10^{9} + 7\) 取模后的值。

输入格式

本题含多组数据,第一行为一个正整数 \(t\) ,代表数据组数。

之后 \(t\) 行分别为一个正整数 \(n\) ,意义如题目所述。

输出格式

对每组数据,输出一个正整数 \(ans\) ,其中 \(ans = \sum_{i = 1}^{n}f(i)\) 对 \(10^{9} + 7\) 取模。

输入输出样例

样例输入 #1

6
1
2
3
4
10
100000000000000

样例输出 #1

2
5
7
10
26
366580019

数据范围与约定

对于 \(30\%\) 的数据, \(n \le 100\) , \(t = 10\)。
对于另外 \(30\%\) 的数据, \(n \le 10^{6}\)。
对于 \(100\%\) 的数据, \(n \le 10^{16}, t \le 10^{4}\)。


T1 难题

考虑求出 \(f=i\) 的数的个数
发现对于 \(f=i\) 的数 有:
是 \(lcm(1,...,i-1)\) 的倍数 且不为 \(lcm(1,....,i)\) 的倍数
于是考虑每次更新 前缀 \(LCM\) 时 , \(i\) 对 \(LCM\) 的贡献为 \(Now\)

去除了已经算过贡献 也就是已经被打上标记的数后
第 \(Now\) 的整数倍的数 为 \(LCM\) 的倍数
则 每 \(Now\) 个数 ,前 \(Now-1\) 个都为 \(f=i\) 的数
\(ans+=(n/Now*(Now-1)+n%Now)*i\)
每轮删掉上次提供贡献的数 \(las\)

因为 \(lcm(1,....,42)>1e18\)
所以代码 为 \(O(t*42)\)或 \(O(t*logn)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define ac read()
#define N 10004
#define Mod ((int)(1e9)+7)
using namespace std;
inline ll read() {
	ll x=0,f=1;
	int c=getchar();
	while(!isdigit(c)) {
		f=(c=='-')?-1:1;
		c=getchar();
	}
	while(isdigit(c)) {
		x=(x<<3)+(x<<1)+(c^48);
		c=getchar();
	}
	return x*f;
}
int T;
ll n;
ll ans;
inline ll MOD(ll a) {
	return (a+Mod)%Mod;
}
inline ll Lcm(ll a,ll b) {
	ll A=a/__gcd(a,b),B=b/__gcd(a,b);
	return A*B*__gcd(a,b);
} 
int main() {
	T=ac;
	for(int i=1; i<=T; ++i) {
		n=ac;
		ans=0;
		ll las=0,now=1,Now=1;
		ll nn=n;
		for(ll j=2; j<=nn+1; ++j) {
			now=Lcm(now,j),Now=now/Now;
			if(Now==1) {
				Now=now;
				continue;
			}
			n-=las;
			if(n<=0) break;
			las=(n/Now*(Now-1))+(n%Now);
			ans=MOD(ans+las*j);
			Now=now;
		}
		cout<<ans<<"\n";
	}
	return 0;
}

矩阵游戏

题目描述

约翰和奶牛们正在散步,忽然看到了路边有一个矩阵,仔细看看发现是一个矩阵游戏。

具体地,这是一个 \(n\) 行 \(m\) 列的矩阵,将进行恰好 \(k\) 次操作,每次操作将矩阵的某一行或某一列的所有元素的值减 \(p\) ,得分为修改前这一行或这一列的元素和。

约翰陷入了苦思,但奶牛们一眼就看出了最优策略,你能帮约翰求出 \(k\) 次操作能获得的分数的最大值吗?

输入格式

第一行为四个整数 \(n,m,k,p\) ,意义如上。

接下来 \(n\) 行每行 \(m\) 个整数 \(a_{i,j}\) 为矩阵的各个元素。

输出格式

输出一个整数表示答案。

输入输出样例

样例输入 #1

2 2 2 1
3 2
4 1

样例输出 #1

11

样例输入 #2

2 2 5 2
1 3
2 4

样例输出 #2

11

数据范围与约定

对于 \(10\%\) 的数据, \(p = 0\)。
对于另 \(20\%\) 的数据, \(m + n \le 10\) , \(k \le 5\)。
对于另 \(20\%\) 的数据,保证最优策略中的操作全为行操作。
对于全部数据, \(n,m \le 10^{3}\) , \(a_{i,j} \le 10^{3}\) , \(k \le 10^{6}\) , \(p \le 100\)。


T2 矩阵游戏

跑暴力发现 行列之间的顺序不影响 答案

再根据部分分 最优只有行的提示

我们可以预处理前缀 \(sum1[i]\) \(sum2[i]\)
分别表示 \(i\) 次行操作的贡献 和 \(i\) 次列操作的贡献
然后枚举行操作 取 \(max\)

Code
#include<bits/stdc++.h>
#define ll long long
#define ac read()
#define N 1005
#define M 1000005
#define int long long
using namespace std;
inline ll read() {
	ll x=0,f=1;
	int c=getchar();
	while(!isdigit(c)) {
		f=(c=='-')?-1:1;
		c=getchar();
	}
	while(isdigit(c)) {
		x=(x<<3)+(x<<1)+(c^48);
		c=getchar();
	}
	return x*f;
}
int n,m,k,p;
int a[N][N],b[N][N];
int ha[N],li[N];
inline void sub1() { // p==0
	sort(ha+1,ha+1+n);
	sort(li+1,li+1+m);
	ll Max=max(ha[n],li[m]);
	cout<<1ll*Max*k<<"\n";
	return ;
}
ll ans=-1e9;
int Caoz[N];
inline void dfs(int x) {
	if(x==k+1) {
		ll sum=0;
		for(int i=1; i<=n; ++i) {
			for(int j=1; j<=m; ++j) {
				a[i][j]=b[i][j];
			}
		}
		for(int K=1; K<=k; ++K) {
			int C=Caoz[K];
			if(C<=n) {
				for(int j=1; j<=m; ++j) {
					sum+=a[C][j];
					a[C][j]-=p;
				}
			} else {
				for(int i=1; i<=n; ++i) {
					sum+=a[i][C-n];
					a[i][C-n]-=p;
				}
			}
		}
		ans=max(ans,sum);
		return ;
	}
	for(int i=n+m; i>=1; --i) {
		Caoz[x]=i;
		dfs(x+1);
	}
	return ;
}
inline void sub2() {
	dfs(1);
	cout<<ans<<"\n";
	return ;
}
struct Han {
	int id,num;
};
bool operator < (Han a,Han b) {
	return a.num<b.num;
}
ll sum1[M],sum2[M];
inline void sub3() {
	priority_queue<Han> q;
	for(int i=1; i<=n; ++i) {
		q.push({i,ha[i]});
	}
	ans=0;
	for(int i=1; i<=k; ++i) {
		Han u=q.top();
		q.pop();
		if(ha[u.id]!=u.num) continue;
		ans+=ha[u.id];
		sum1[i]=ans;
		ha[u.id]-=p*m;
		q.push({u.id,ha[u.id]});
	}

	priority_queue<Han> q1;
	for(int i=1; i<=m; ++i) {
		q1.push({i,li[i]});
	}
	ans=0;
	for(int i=1; i<=k; ++i) {
		Han u=q1.top();
		q1.pop();
		if(li[u.id]!=u.num) continue;
		ans+=li[u.id];
		sum2[i]=ans;
		li[u.id]-=p*n;
		q1.push({u.id,li[u.id]});
	}
	ans=-1e18;
	for(int i=0; i<=k; ++i) {
		ans=max(ans,sum1[i]+sum2[k-i]-1ll*p*i*(k-i));
	}
	cout<<ans<<"\n";
	return ;
}
signed main() {
	n=ac,m=ac,k=ac,p=ac;
	for(int i=1; i<=n; ++i) {
		for(int j=1; j<=m; ++j) {
			a[i][j]=ac;
			ha[i]+=a[i][j];
			li[j]+=a[i][j];
			b[i][j]=a[i][j];
		}
	}
	sub3();
	return 0;
}

括号序列

题目描述

奶牛们正在玩一些括号,定义括号序列为只包括 ( 和 ) 的字符串。奶牛数学家定义一个匹配的括号序列满足:可以在其中加入 1 和 +,将其转化为合法的代数式,例如:
( ) 和 ( ( ) ) 是匹配的
) ( 和 ( 和 ) 不是。

他们还将两个字符串拼接在一起简记为 \(x + y\) 。例如, (1)() + () = (1)()() 。

奶牛们研究透了之后,给了约翰 \(n\) 个括号序列 \(s_1 \dots s_n\) ,可以任意改变他们的顺序,要求使得最终改变顺序后的字符串满足 \(s_1 + s_2 + \dots +s_n\) 的匹配的括号序列前缀个数最多,你需要输出这个个数。

输入格式

第一行包含一个正整数 \(n\) 。

接下来的 \(n\) 行,第 \(i\) 行包含一个括号序列 \(s_i\)。

输出格式

输出一个整数,表示任意顺序下拼接后的最多的匹配的括号序列前缀个数。

输入输出样例

样例输入 #1

2
(
)

样例输出 #1

1

样例输入 #2

4
(
(
)
)

样例输出 #2

4

样例输入 #3

1
()

样例输出 #3

1

样例输入 #4

1
)(

样例输出 #4

0

数据范围与约定

对于 \(20\%\) 的数据, \(n \le 5\) , \(\sum |s_i| \le 20\)。
对于另 \(20\%\) 的数据, \(|s_i| = 1\)。
对于 \(100\%\) 的数据, \(n \le 20\) , \(\sum |s_i| \le 4 \times 10^5\)。


T3 括号序列

\(n=20\) 明显提示 状压
考虑预处理每个串的总价值 \(val\)(左括号数-右括号数)
以及 在串内时最小值 \(Min\)

\(cnt[i][j]\) 表示接上第 \(i\) 个串前 总价值为 \(j\) ,接上第 \(i\) 个串后没有出现价值达到 \(<0\) 的价值为 \(0\) 的前缀个数

然后预处理 \(sum[S]\) 表示选择字符串情况为 \(S\) 时的总价值

Code

#include<bits/stdc++.h>
#define ll long long
#define ac read()
#define M 400005
using namespace std;
inline int read() {
   int x=0,f=1;
   int c=getchar_unlocked();
   while(!isdigit(c)) {
   	f=(c=='-')?-1:1;
   	c=getchar_unlocked();
   }
   while(isdigit(c)) {
   	x=(x<<3)+(x<<1)+(c^48);
   	c=getchar_unlocked();
   }
   return x*f;
}
int n;
char s[26][M];
int f[(1<<21)],cnt[26][M];
//准备接上第 i 个串,当前的 左括号-右括间号 = val 时
// 这个串能贡献几次合法前缀为 -val 的区间
struct Seq {
   int val,Min;
} seq[26];
int sum[(1<<21)],ans=0;
int main() {
   n=ac;
   for(int i=1; i<=n; ++i) {
   	scanf("%s",s[i]+1);
   	int Min=1e9,len=strlen(s[i]+1),val=0;
   	for(int j=1; j<=len; ++j) {
   		int v=((s[i][j]=='(')?1:-1);
   		val+=v;
   		Min=min(Min,val);
   		if(Min>=val&&val<=0) ++cnt[i][-val];//预处理合法的 cnt
   	}
   	//计算遍历每个串时的最小值 和 整个串的价值
   	seq[i].Min=Min,seq[i].val=val;
   }
   memset(f,-1,sizeof f);
   f[0]=0;
   int SS=(1<<n);
   for(int S=1; S<SS; ++S) {
   	for(int i=0; i<n; ++i) {
   		if((S>>i)&1) {// 预处理当前情况为 S 时,sum 的值
   			sum[S]=sum[S^(1<<i)]+seq[i+1].val;
   			break;
   		}
   	}
   }
   for(int S=1; S<SS; ++S) {
   	for(int i=0; i<n; ++i) {
   		if(!((S>>i)&1)) continue;
   		int last=S^(1<<i);//之前的状态
   		if(f[last]==-1) continue;
   		if(sum[last]+seq[i+1].Min>=0) {
   			// 如果加入后仍然可以保持
   			// 有机会出现合法前缀括号区间
   			// 就更新
   			f[S]=max(f[S],f[last]+cnt[i+1][sum[last]]);
   		} else {
   			//否则直接更新答案
   			ans=max(ans,f[last]+cnt[i+1][sum[last]]);
   		}
   	}
   	//每次更新完这个状态 S, 就拿它来更新答案
   	ans=max(ans,f[S]);
   }
   cout<<ans<<"\n";
   return 0;
}

路程

题目描述

小硫和小氟在一张图上玩游戏。

小硫不喜欢环,所以这张图是一个DAG。

小氟要在这个图上行走,他遵循本心,所以起点是 \(1\) ,他仰望前辈,所以终点是 \(114\) 。

小硫给这个图的每一条边都划定了边权,小氟每走过一条从 \(1\) 到 \(114\) 的路径,所得奖励就是这条路径上的边权之和。

小硫想要小氟选择一条路径走过去,得到奖励。

小孩子才做选择呢...我全都要!

小氟发现,这张图正好有 \(n + 1\) 条从 \(1\) 到 \(114\) 的路径,并且在这些路径的边权和中, \([0,n]\) 的每一个整数正好出现了一次。

小氟想问你,这张图可能是什么样的?请你给出一种方案。

所有的点编号和边权皆为非负整数,点的编号要在 \([1,114]\) 范围内。

输入格式

只有一个整数 \(n\) 。

输出格式

第一行两个正整数,分别为点数和边数。

接下来每一行输出三个正整数 \(x,y,z\) ,代表一条有向边的起点、终点、和边长。

输入输出样例

样例输入 #1

13

样例输出 #1

2 14
1 114 0
1 114 1
1 114 2
1 114 3
1 114 4
1 114 5
1 114 6
1 114 7
1 114 8
1 114 9
1 114 10
1 114 11
1 114 12
1 114 13

数据范围和约定

对于 \(10\%\) 的数据,有 \(n \le 40\)。
对于 \(30\%\) 的数据,有 \(n \le 120\)。
对于 \(30\%\) 的数据,有 \(n = 2^{k}, k \in \mathbb{Z}\)。
对于 \(100\%\) 的数据,有 \(n \le 32500\) ,方案中点数 \(\le 18\) ,边数 \(\le 45\) ,边权 \(\le n\)。
其中点数包含 \(1\) 和 \(114\)。

【SP使用】

运行程序后,在程序所在文件夹中打开cmd,输入以下命令:
checker 输入文件 选手输出文件 标准输出文件 10 score.txt report.txt

例如输入为 road.in,选手输出为 road.out,标准输出为 road1.out(其实没什么用),就是:
checker road.in road.out road1.out 10 score.txt report.txt

就可以在 score.txt 中查看得分,report.txt 中查看相关得分的描述。

T4 路程

#include<bits/stdc++.h>
#define ll long long
#define ac read()
using namespace std;
inline ll read() {
	ll x=0,f=1;
	int c=getchar();
	while(!isdigit(c)) {
		f=(c=='-')?-1:1;
		c=getchar();
	}
	while(isdigit(c)) {
		x=(x<<3)+(x<<1)+(c^48);
		c=getchar();
	}
	return x*f;
}
int n;
int N,M;
struct node {
	int u,v,val;
} e[200];
int main() {
	n=ac;
	N=2;
	if(n<=40) {
		M=n+1;
		cout<<N<<" "<<M<<"\n";
		for(int i=0; i<M; ++i) {
			cout<<"1 "<<"114 "<<i<<"\n";
		}
		return 0;
	}
	int bo=1;
	int last=0,BO=-1;
	// 把 n 二进制拆分
	// 从高位到低位 ,最高的 1 第 i(从 0开始)位,建一条主链
	// 就是 1 到 i之间连 0 和 (1<<j), i 和 114 之间 连 1 和 0
	// 从大到小
	// 在后面遇到 1 时, 就从 1 连一条对应的边到主链上
	/*
	n=10
	(1010)_2
	i=3 {
		    4       2	    1
		1 < = > 2 < = > 3 < = > 114
	  	    0       0       0
		las+=(1<<i);
	}
	这条主链只满足了[0~2^4)
	i=1{
	        4       2       1
		1 < = > 2 < = > 3 < = > 114
		 \	0       0  /\   0
	      \___________/
			   las
		las+=(1<<i);
	}
	最后发现 这个竟然没有 边权和为 n 的路径
	直接连一条 1 --> 114
	       _____________________
		  /         10          \
	     /   4       2       1  \/
		1 < = > 2 < = > 3 < = > 114
		 \	0       0  /\   0
	      \____________/
			   las
	*/
	// n<=32500
	// Max_I=15
	// Max_tot = 15*2 + 15
	// 总边数 <=45  点数 <=18 满足题目限制
	for(int i=15; i>=0; --i) {
		if(!((n>>i)&1)) continue;
		if(bo==1) {
			N+=i-1;
			for(int j=1; j<i; ++j) {
				e[++M]= {j,j+1,(1<<(i-j))};
				e[++M]= {j,j+1,0};
			}
			e[++M]= {i,114,1};
			e[++M]= {i,114,0};
			bo=0;
			BO=i;
		} else {
			if(i==0) {
				e[++M]= {1,114,last};
			} else {
				e[++M]= {1,BO-i+1,last};
			}
		}
		last+=(1<<i);
	}
	e[++M]= {1,114,n};
	cout<<N<<" "<<M<<"\n";
	for(int i=1; i<=M; ++i) {
		cout<<e[i].u<<" "<<e[i].v<<" "<<e[i].val<<"\n";
	}
	return 0;
}
posted @ 2026-11-13 20:55  Lonely_Sun  阅读(3)  评论(0)    收藏  举报