【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;
}

浙公网安备 33010602011771号