济南 CSP-J 刷题营 Day1 基础算法
Solution
T1 方差
原题链接
简要思路
利用题目中给定的公式进行计算即可,但是要加一些小优化来以防 TLE:我们要将所有的计算(包括和、平方的计算等)都用前缀和的思想来对其存储下来,这样就可以达到 O(1) 的查询复杂度。
针对分数的问题,只有平均数和方差的结果可能为分数,所以我们对其分别存下分子和分母。最后计算完毕后,如果分母能够整除分子,就输出分母除以分子;否则取分母分子的最大公约数,让它们都除上这个最大公约数,以达到将分数化至最简的要求。
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int gcd(int a,int b){
if(b==0)return a;
return gcd(b,a%b);
}
int qzh[MAXN];//前缀和
int pjs_fenzi[MAXN],pjs_fenmu[MAXN];//平均数的分子和分母
int fc_fenzi[MAXN],fc_fenmu[MAXN];//方差的分子和分母
int pf[MAXN];//平方(针对方差中平方的计算)
int n,a[MAXN];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
qzh[i]=qzh[i-1]+a[i];
for(int i=1;i<=n;i++){
pjs_fenmu[i]=i;
pjs_fenzi[i]=qzh[i];
}
for(int i=1;i<=n;i++)
pf[i]=pf[i-1]+a[i]*a[i];
for(int i=1;i<=n;i++){
fc_fenzi[i]=pf[i]*i-qzh[i]*qzh[i];
fc_fenmu[i]=i*i;
}
for(int i=1;i<=n;i++){
cout<<qzh[i]<<' ';
if(pjs_fenzi[i]%pjs_fenmu[i]==0)cout<<pjs_fenzi[i]/pjs_fenmu[i]<<' ';
else{
int g=gcd(pjs_fenzi[i],pjs_fenmu[i]);
cout<<pjs_fenzi[i]/g<<'/'<<pjs_fenmu[i]/g<<' ';
}
if(fc_fenzi[i]%fc_fenmu[i]==0)cout<<fc_fenzi[i]/fc_fenmu[i]<<endl;
else{
int g=gcd(fc_fenzi[i],fc_fenmu[i]);
cout<<fc_fenzi[i]/g<<'/'<<fc_fenmu[i]/g<<endl;
}
}
return 0;
}
T2 查询
原题链接
简要思路
考虑二分答案。
在 \(j\) 固定的情况下,\(c_i\) 越大,我们要求的 \(a_j + b_j × c_i\) 就越大。
设要求的第 \(k\) 大的值为 \(S\)。可以先对 \(c_i\) 排序,然后用二分来查找 \(S\)。
求 \(\sum\limits_{i=1}^{i\leq n} \sum\limits_{j=1}^{j\leq n} \left[ a_j+b_j × c_i \leq S \right]\)。于是现在我们就可以对每个 \(j\) 二分出有多少个 \(i\) 使得 \(a_j+b_j × c_i \leq S\),因此我们现在就可以在 O(n log n) 的时间复杂度内求出值 \(\leq S\) 的二元组的个数。如果求出来的值 \(\geq k\) 就返回 true,否则返回 false。
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+5;
int a[MAXN],b[MAXN],c[MAXN],n,k;
bool check(int x){
int num=0;//记录有多少个二元组的值比 x 小
for(int i=1;i<=n;i++)
if(x-a[i]>=0)//保证比 a[i] 大
num+=upper_bound(c+1,c+n+1,(x-a[i])/b[i])-c-1;//二分查看有多少比其小的
return num>=k;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
for(int i=1;i<=n;i++)cin>>c[i];
sort(c+1,c+n+1);//对 c 数组进行排序
cin>>k;
int l=0,r=2e18;
while(l<r){//二分答案
int mid=(l+r)>>1;//位运算 >>1 相当于 /2
if(check(mid))r=mid;//答案在左区间
else l=mid+1;//答案在右区间
}
cout<<r<<endl;
return 0;
}
T3 枚举
原题链接
简要思路
-
No.1 30pts
枚举所有的 \(a,b,c\)。时间复杂度 O(\(P^3\))。
-
No.2 65pts
可以通过逆推得到 \(N_2\) 的所有取值及其对应的 \(c\),然后同理可以得到所有 \(N_1\) 的取值及其对应的 \(a\).最后,我们对于每一个 \(N_1\) 的取值都可以二分出所有合法的 \(N_2\) 的个数以及其对应的方案。
时间复杂度 \(O\)(\(P^2\) log \(P\))。
-
No.3 100pts
通过式子:\(\lfloor N_2 / c \rfloor \times c + N_3 = N_2\) 我们可以发现其实求 \(N_1\) 的值其实等同于算法二。我们可以枚举 \(c\) 的值和 \(\lfloor N_2 / c \rfloor\) 的值来得到 \(N_2\),然后用二分或者前缀和来得到合法的 \(b\)。
时间复杂度 \(\sum\limits_{c=1}^{P} \lfloor P / c \rfloor = O\)(\(P\) log \(P\))。
完整代码
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int MAXN=2e5+5;
struct node{
int x;
int y;
int z;
};
vector<node> vec;
vector<int> h[MAXN];
int N,N0,N3;
int p,sum[MAXN],nxt[MAXN];
signed main(){
cin>>N>>N3>>p;
for(int i=0;i<=p*2;i++)//清空
h[i].clear();
for(int c=N3+1;c<=p;c++){//枚举所有可能的 c
for(int k=0;N3+c*k<=p+p;k++){
sum[N3+c*k]++;
h[N3+c*k].push_back(c);
}
}
nxt[p+p+1]=1e9;
for(int i=p+p;i>=0;i--){
sum[i]+=sum[i+1];
if(h[i].empty()) nxt[i]=nxt[i+1];
else nxt[i]=i;
}
int ans=0;
for(int a=1;a<=p;a++){//枚举所有可能的 a
int N1=(N*N+1)%a;
ans+=sum[N1]-sum[N1+p+1];
for(int N2=nxt[N1];N2<=N1+p;N2=nxt[N2+1]){
if(vec.size()==100000) break;
for(int j=0;j<h[N2].size();j++){
vec.push_back({a,N2-N1,h[N2][j]});
if(vec.size()==100000) break;
}
}
}
cout<<ans<<endl;
for(int i=0;i<vec.size();i++){
cout<<vec[i].x<<' '<<vec[i].y<<' '<<vec[i].z<<endl;
}
return 0;
}
T4 染色
原题链接
简要思路
-
No.1 28pts
暴力枚举划分。
-
No.2 52pts
我们可以发现划分一定是形如
RRRRR...BRBRBR...BBBBB...的形式,通过枚举三段的长度求值可以得到 O(\(n^4\)) 或 O(\(n^3\)) 的做法。 -
No.3 100pts
当起点 \(s\) 和第一段R的终点固定时,BR的长度越长,\(\sum\limits_{i=1}^{p}a_{r_i} - \sum\limits_{j=1}^{p}a_{b_j}\),因此只在临界点附近才可能取到最小值。当第一段的
R的终点固定,起点增加时,临界点的位置只会向后移动,并不会向前。因此我们就可以对每一段
R的终点,维护到达临界点时BR的长度,当 \(s\) 移动时可以利用单调性来维护对应的位置。时间复杂度 O(\(n^2\))。
完整代码
(暂时未完全理解)。
#include<bits/stdc++.h>
#define int long long;
#define endl '\n'
using namespace std;
const int MAXN=2e5+5;
struct K{
int sum;
int cnt;
}f[MAXN];
int n,sum;
int a[MAXN];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
a[n+i]=a[i];
sum+=a[i];
}
f[1].sum=2*a[1]-sum;
f[1].cnt=0;
for(int i=2;i<=n;i++){
f[i].sum=f[i-1].sum+2*a[i];
f[i].cnt=0;
}
for(int s=1;s<=n;s++){
if(s>1){
for(int j=1;j<=n-1;j++){
f[j]=f[j+1];
f[j].sum-=2*a[s-1];
}
}
int ans=1e18,cnt=0;
for(int j=1;j<=n-1;j++){
int f1=j+(f[j].cnt+1)*2;
int f2=abs(f[j].sum+a[s+j+f[j].cnt*2+1]*2);
int f3=abs(f[j].sum)
while(f1<=n-1&&f2<f3){
f[j].sum+=a[s+j+f[j].cnt*2+1]*2;
f[j].cnt++;
f1=j+(f[j].cnt+1)*2;
f2=abs(f[j].sum+a[s+j+f[j].cnt*2+1]*2);
f3=abs(f[j].sum)
}
if(abs(f[j].sum)<ans) cnt=f[j].cnt;
ans=min(ans,abs(f[j].sum));
}
cout<<ans<<endl;
}
return 0;
}
讲课笔记
枚举
从可能得集合中一一尝试统计贡献。
模拟
模拟题目中要求的操作
例题:NOIP2014 生活大爆炸版石头剪刀布
原题链接:洛谷 P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布
注意到赢了是得 \(1\) 分,平局和输都是 \(0\) 分,所以我们直接根据题意打表。
int Vs[5][5]={{0,0,1,1,0},{1,0,0,1,0},{0,1,0,0,1},{0,0,1,0,1},{1,1,0,0,0}};
然后我们又发现是循环出拳,所以我们用取模的方法来实现。
完整代码:
#include <bits/stdc++.h>
using namespace std;
int n,lena,lenb;//题面所示
int a[205],b[205];//两个人的出拳顺序
int pointa,pointb;//两个人的得分情况
int Vs[5][5]={{0,0,1,1,0},{1,0,0,1,0},{0,1,0,0,1},{0,0,1,0,1},{1,1,0,0,0}};//打表
signed main(){
cin>>n>>lena>>lenb;
for(int i=0;i<lena;i++)cin>>a[i];
for(int i=0;i<lenb;i++)cin>>b[i];
for(int i=0;i<n;i++){
pointa+=Vs[a[i%lena]][b[i%lenb]];
pointb+=Vs[b[i%lenb]][a[i%lena]];//取模
}
cout<<pointa<<' '<<pointb<<endl;
return 0;
}
例题:CODECHEF PAINTING
-
题面:
一个无穷大的网格,机器人初始在 \(a_{0,0}\) 上。进行 \(n\) 次操作,每次操作给定一个字符 \(s\) 和一个代表距离的正整数 \(d\)。字符共有四种可能,U代表向上,D代表向下,R表示向左,L代表向右。机器人会向相应的方向行进 \(d\) 个单位长度,并将其经过的位置染为白色。问每次移动后新增加的白色格子的数量。 -
注意点:
- 一个格子可能被重复染色,但是我们只能计算一次(第一次)。
- 但是不能挨个枚举走过的每个格子,会 TLE。
- 因此得到一个大致的思路:
- 依次计算每次路径走过的格子的数量。
- 枚举之前的路径计算交集。
- 对交集进行处理,求已经被染色的格子的数量。
- 用总共经过的格子的数量减去交集的格子的数量就是我们每次要求的答案。
- 计算交集
- 如果是计算横的路径和竖的路径的交集,只可能是有一个格子或者是空集。
- 如果同竖或同横的话,两条路径同行(列)交集为空或者是该行(列)的一个区间,不同行(列)交集就为空。
- 整合
我们通过上述分讨发现两条路径之间的交集不是为空就是该路径上的一个区间(一个点也算区间),对这些交集求并后可以在排序后将重复的部分合并起来。
小结
-
用到模拟和枚举的题细节复杂且多,需要注意读题和模拟样例。
-
先写代码框架后对其拆分成更细的模块,进行补充。
递推
通过若干步可重复的运算来计算复杂问题的方法。
例题:台阶问题
原题链接:洛谷 P1192 台阶问题
令 \(a_i\) 为到达第 \(i\) 级台阶的方案数,对于每一个 \(a_i\),我们可以枚举 \(a_{i-1}\) 的方案数。
由此我们可以得到递推式:
\(a_0=1\);
\(a_i=\sum_{j=1}^{min(i,k)} a_{i-j}\)。
完整代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e5+3;
int n,k;
long long a[mod];
signed main(){
cin>>n>>k;
a[0]=a[1]=1;
for(int i=2;i<=n;i++){
if(i<=k)a[i]=(a[i-1]*2)%mod;
else a[i]=(a[i-1]*2-a[i-k-1])%mod;
}
cout<<(a[n]+mod)%mod<<endl;//别忘了加上再模一次
return 0;
}
贪心
-
每次做出一个选择后将原问题转化为相似的、规模更小的子问题求解。
-
证明时一般先假设所有的最优解都不包括这个局部最优解,然后将其中的一部分替换为该局部选择使得答案不劣。重复上述过程直至找到答案。
例题:分数背包
\(n\) 个物品,第 \(a_i\) 个物品的体积为 \(v_i\) 价值为 \(w_i\)。我们可以对物品进行切割,如果切割出的体积为 \(x\),则其价值为 \(w_i × (x/v_i)\),求体积不超过 \(m\) 的物品的最大价值和。
每次取性价比最大的物品,所以按照 \(w_i / v_i\) 排序即可。(具体证明见 PPT)
事实上这道题还可以用类似找中位数的方法直接 O(n) 计算答案
例题:\(k\) 叉哈夫曼树
\(n\) 个权值非负的元素,每次可以合并其中不超过 \(k\) 个元素(和合并果子有点类似,但是只能合并 \(2\) 个数),合并后新产生的元素为这些元素之和,并产生同样数值的代价。求将这些元素合并成一个的代价。
-
做法
补 \(0\) 到使 \(n-1\) 是 \(k-1\) 的倍数,然后每次取最小的 \(k\) 个合并(小的数合并多次,大的数少合并几次)。 -
证明
假设一定存在最优解不包含该决策,那么假设存在儿子个数不为 \(k\) 的非叶子节点,可以将深度大的叶子补到深度小的空位上;如果没有一个非叶子节点儿子是前 \(k\) 小的,可以将一个前 \(k\) 小的与一个深度最大的节点替换。
例题:The Smallest String Concatenation
原题链接:洛谷 CF632C The Smallest String Concatenation
用手写 cmp 和 sort 就简单的解决了。
#include <bits/stdc++.h>
using namespace std;
string s[3000000];
bool cmp(string a,string b){//手写 cmp 比较器
return a+b<b+a;//比较两种拼接方式的大小
}
int n;
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
sort(s+1,s+n+1,cmp);
for(int i=1;i<=n;i++) cout<<s[i];
return 0;
}
二分
-
二分查找是指在一个有序数组中查找某一元素的算法,在 C++ 中可以运用的算法是
lower_bound和upper_bound。 -
更一般的二分可以看做是对一个具有有序性质的量进行不断划分,以此来简化问题。也就是说,如果我们知道一个广义数组的左侧不满足某一条件,而右侧满足该条件,那么就可以用二分的方法来查找第一个
例题:Keshi Is Throwing a Party
原题链接:洛谷 CF1610C Keshi Is Throwing a Party
- 做法
题目中提到最多,自然就会想到二分答案,可是要怎么判断当前的答案是否符合条件呢?题目中提到第 \(i\) 个朋友的财产为 \(i\),可以发现他们的财产也具有单调性,也就意味着对于第 \(i\) 个人来说,第 \(1 \sim (i-1)\) 个人的财产必然小于他,第 \((i+1) \sim n\) 个人的财产必然大于他。假设总人数为 \(N\),那么当选择了 \(x\) 个人时,只需要考虑 \(b_i\) 是否大于 \(x\),\(a_i\) 是否大于 \(N-x-1\)。
- 代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int MAXN=2e5+5;
int T,n;
int a[MAXN],b[MAXN];
bool check(int x){
int num=0;
for(int i=1;i<=n;i++)
if(a[i]>=x-num-1 && b[i]>=num)num++;
return num>=x;
}
signed main(){
cin>>T;
while(T--){
int ans=0;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i]>>b[i];
int l=1,r=n;
while(l<=r){
int mid=(l+r)/2;
if(check(mid)) l=mid+1,ans=mid;
else r=mid-1;
}
cout<<ans<<endl;
}
return 0;
}
习题题单
| 题目 | 完成情况 |
|---|---|
| P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布 | \(\color{yellowgreen}{√}\) |
| P1192 台阶问题 | \(\color{yellowgreen}{√}\) |
| CF632C The Smallest String Concatenation | \(\color{yellowgreen}{√}\) |
| CF1610C Keshi Is Throwing a Party | \(\color{yellowgreen}{√}\) |
当日总结
-
加速和快读快输不要同时使用,否则会挂零。
-
看到 \(1e5\) 这种不大也不小的数据时,要自然而然地想到二分。
-
如果题目中出现
最大值最小或最小值最大之类的字眼时,一般要用二分答案。 -
二分答案注意主函数部分的调用:加一减一,小于还是小于等于......
-
学会通过条件进行逆推。
-
要善于把抽象的题转变为善于理解的具象的题面再下手。

浙公网安备 33010602011771号