CUSTACM Summer Camp 2022 Training 7
A. Platforms Jumping
题意
要从0到n+1这个点,中途有m块踏板,可以改变踏板的位置但不能改变相对位置,主人公可以移动的距离是[1,d],问如何到达,或者不能到达
数据范围: 1 ≤ n , m , d ≤ 1000 1 \leq n,m,d \leq 1000 1≤n,m,d≤1000
tags:模拟,思维
思路
先把所有的木板都一起放在右边,然后一块一块移到左边直到可以跳到右边来
- disp[maxn]维护每个木板可以提供的最远距离(前缀和)
- sum求出所有木板的长度,并把所有木板都放在右边
- need维护需要跨越的距离,如果d不够,就从左边那一块木板来并更新need
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
int n,m,d;
int c[maxn],dis[maxn],out[maxn];
int sum;
void solve(){
if(dis[m]+d<n+1){//如果dis[m]+d<n+1,说明所有木板可以提供的最远距离不够,cout<<"NO";
cout<<"NO"<<endl;
return;
}
else cout<<"YES"<<endl;
int need=n-sum;
int x,y;
for(int i=1;i<=m;i++){
if(dis[i]<need+c[i]){
need+=c[i];
while(c[i]--)out[dis[i]--]=i;
}
else if(dis[i]>need+c[i]){
// while(dis[i]>need+c[i])dis[i]--;
dis[i]=need+c[i];
x=i+1,y=need+c[i]+1;
while(c[i]--)out[dis[i]--]=i;
break;
}
else{
x=i+1,y=need+c[i]+1;
while(c[i]--)out[dis[i]--]=i;
break;
}
}
// cout<<x<<' '<<y<<endl;
for(int i=x;i<=m;i++){
while(c[i]--)out[y++]=i;
}
for(int i=1;i<=n;i++)cout<<out[i]<<' ';
cout<<endl;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>d;
for(int i=1;i<=m;i++){
cin>>c[i];
dis[i]=dis[i-1]+d+c[i]-1;
// cout<<dis[i]<<' ';
sum+=c[i];
}
solve();
}
复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
B. k-LCM (hard version)
题意
长度为 k 的数组,整个数组的和为 n ,整个数组的最小公倍数不超过 n / 2,让你构造这样一个数组
简单版本k=3,困难版本 3 <= k <= n。数据范围: 3 ≤ n ≤ 1 0 9 , 3 ≤ k ≤ 1 0 5 3 \leq n \leq 10^9,3 \leq k \leq 10^5 3≤n≤109,3≤k≤105
tags:构造,思维
思路
从最小公倍数不大于n/2出发,让一组数的最小公倍数指向我们预期的值n/2(或n/2-1)
对于简单版本,k=3时
- 若n为奇数,那么可以构造成
n/2,n/2,1
- 若n为偶数
- 若n/2为奇数,那么可以构造成
n/2-1,n/2-1,2
- 若n/2为偶数,那么可以构造成
n/2,n/4,n/4
对于困难版本,无非就是增加了数组的长度,但是1对最小公倍数没用影响,于是取k-3个1,把k降到3然后用上面的思路
代码
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
int n,k;
cin>>n>>k;
if(k>3){
for(int i=0;i<k-3;i++)cout<<1<<' ';
n-=k-3;
k=3;
}
if(n&1){
cout<<n/2<<' '<<n/2<<' '<<1<<' ';
}
else {
if((n/2)&1){
cout<<n/2-1<<' '<<n/2-1<<' '<<2<<' ';
}
else cout<<n/4<<' '<<n/4<<' '<<n/2<<' ';
}
cout<<endl;
}
}
复杂度: O ( k ) O(k) O(k)
C. You Are Given a Decimal String…多源最短路径好题
题意
给定的字符串是机器打印出来的子序列,而这个子序列是计算器上每次计算后的个位数,求0~9中第i行第j列表示i-j计算器,只能加i或者加j,求出现这个子序列最少需要插入多少个数?(或者理解为最少要用多少次计算器)(以矩阵形式输出,若无发成功则输出-1)
数据范围: 1 ≤ s ≤ 2 ∗ 1 0 6 1 \leq s \leq 2*10^6 1≤s≤2∗106
tags:思维,Floyd多源最短路径
思路
对于给定子序列 s 1 s 2 s 3 . . . s k s_1s_2s_3...s_k s1s2s3...sk,我们需要做的是把 子问题:添加一些其他数(路径) a 1 , a 2 . . . ,使 s i (起点)到 s i + 1 (终点),要求添加的数最少 子问题:添加一些其他数(路径)a_1,a_2...,使s_i(起点)到s_{i+1}(终点),要求添加的数最少 子问题:添加一些其他数(路径)a1,a2...,使si(起点)到si+1(终点),要求添加的数最少
其中添加数的过程如同最短路径中添加点来“松弛”的效果,要求添加的数最少就是添加的路径(长度为1)最少。
而填加的数(路径)可以预处理出来:0-9分别+i和+j之后%10
比如0到(0+i)%10有一条长度为1的路径
1到(1+i)%10有一条长度为1的路径
9到(9+j)%10有一条长度为1的路径
很巧秒的多源最短路径变形
代码
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
string s;
int dis[11][11];
int floyd(int x,int y){
memset(dis,0x3f,sizeof(dis));
for(int i=0;i<10;i++){
dis[i][(i+x)%10]=1;
dis[i][(i+y)%10]=1;
}
for(int k=0;k<10;k++){
for(int i=0;i<10;i++){
for(int j=0;j<10;j++){
dis[i][j]=min(dis[i][k]+dis[k][j],dis[i][j]);
}
}
}
int ans=0;
for(int i=0;i<s.length()-1;i++){
int x=dis[s[i]-'0'][s[i+1]-'0'];
if(x==inf)return -1;
ans+=x-1;
}
return ans;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>s;
for(int i=0;i<10;i++){
for(int j=0;j<10;j++){
cout<<floyd(i,j)<<' ';
}
cout<<endl;
}
}
D. Crazy Diamond
题意
给你一个n个数,通过交换两数使它变为有序,n为偶数
交换规则:若 2 ∗ ∣ i − j ∣ ≥ n 2*|i-j| \geq n 2∗∣i−j∣≥n,可交换pi,pj,并且不要求交换的次数最少但是总次数不能超过5n次
数据大小: 2 ≤ n ≤ 3 ∗ 1 0 5 2 \leq n \leq 3*10^5 2≤n≤3∗105
tags:思维
思路
因为1与n/2到n的数都可以交换,n与1到n/2的数都可以交换,所以当不能直接交换时,就把它换到1或n的位置在交换
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
int n;
int a[maxn];
vector<pair<int,int>>v;
void sw(int x,int y){
v.push_back(make_pair(x,y));
swap(a[x],a[y]);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=n/2+1;i<=n;i++){//先把后半段中<=n/2的部分移到前面去
while(a[i]!=i&&a[i]<=n/2){
if(abs(i-a[i])>=n/2)sw(i,a[i]);
else{
sw(i,1);
sw(1,n);
sw(n,a[n]);
sw(1,n);
sw(i,1);
}
}
}
for(int i=1;i<=n/2;i++){//对1到n/2的数进行排序,把1当作中间节点
if(a[i]==i)continue;
while(a[i]!=i){
sw(i,n);
sw(n,a[n]);
sw(i,n);
}
}
for(int i=n/2+1;i<=n;i++){//对于n/2+1到n的数进行排序,把n当作中间节点
if(a[i]==i)continue;
while(a[i]!=i){
sw(i,1);
sw(1,a[1]);
sw(i,1);
}
}
// for(int i=1;i<=n;i++)cout<<a[i]<<' ';
// cout<<endl;
cout<<v.size()<<endl;
for(int i=0;i<v.size();i++)cout<<v[i].first<<' '<<v[i].second<<endl;
}
E. Skyscrapers
题意
n*m网格,每个网格上有一个数字,对于第(i,j)个格子,要求你改变第i行第j列的数,使第i行和第j列的数相对大小不变而最大值最小,求出该最大值并以矩阵形式输出。注意每次分析都的独立进行的。
数据范围: 1 ≤ n , m ≤ 1000 , 1 ≤ a i , a j ≤ 1 0 9 1 \leq n,m \leq 1000,1 \leq a_i,a_j \leq 10^9 1≤n,m≤1000,1≤ai,aj≤109
tags:思维,二分,离散化
思路
对于每一行每一列,都进行排序,去重操作,然后在对(i,j)分析时,用二分找到a[i],a[j]在上述操作后所在的位置x1,x2,其中该位置肯定是取max(x1,x2),而对于最大值,要先求出最大差值max(y1-x1,y2-x2)(y1、y2是行列单独计算的最大值),最终结果是max(x1,x2)+max(y1-x1,y2-x2);
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5,maxm=1e3+5;
int n,m;
int a[maxn][maxm];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
cin>>a[i][j];
vector<int>hh[maxn],ll[maxm];
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
hh[i].push_back(a[i][j]);
for(int j=0;j<m;j++)
for(int i=0;i<n;i++)
ll[j].push_back(a[i][j]);
for(int i=0;i<n;i++){//对于每行排序去重
sort(hh[i].begin(),hh[i].end());
hh[i].erase(unique(hh[i].begin(),hh[i].end()),hh[i].end());
}
for(int j=0;j<m;j++){//对于每列去重
sort(ll[j].begin(),ll[j].end());
ll[j].erase(unique(ll[j].begin(),ll[j].end()),ll[j].end());
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
int x1=lower_bound(hh[i].begin(),hh[i].end(),a[i][j])-hh[i].begin()+1;//二分找a[i]位置
int y1=hh[i].size();
int x2=lower_bound(ll[j].begin(),ll[j].end(),a[i][j])-ll[j].begin()+1;//二分找a[j]位置
int y2=ll[j].size();
cout<<max(x1,x2)+max(y1-x1,y2-x2)<<' ';
}
cout<<endl;
}
}
复杂度: O ( n ∗ m ∗ ( l o g n + l o g m ) ) O(n*m*(logn+logm)) O(n∗m∗(logn+logm))
F. Trailing Loves (or L’oeufs?)数论好题
题意
计算n!在b进制下的后缀0个数
数据范围: 1 ≤ n ≤ 1 0 18 , 2 ≤ b ≤ 1 0 12 1 \leq n \leq 10^{18},2 \leq b \leq 10^{12} 1≤n≤1018,2≤b≤1012
tags:数论
思路
从特殊问题入手:计算n!在10进制下后缀0的个数
首先任意一个数都可以分解成若干个质因数的和
而对于质因数2和5,2*5=10
而在n!分解为质因数之后2x,5y。x>y是一定的,而出现后缀0的个数取决于y的大小
对于计算y的大小,不用取求出n!,而只用计算n/5,n/25,n/75…的个数之和即可知道n!分解为质因数中5y的y的大小
在来讨论一般性问题:计算n!在b进制下后缀0的个数
上述是10进制下后缀0的个数,我们就凑车2*5=10
同理,这次是b进制下后缀0的个数,我们就凑成 x 1 y 1 ∗ x 2 y 2 ∗ x 3 y 3 . . . = b x_1^{y_1}*x_2^{y_2}*x_3^{y_3}...=b x1y1∗x2y2∗x3y3...=b,其中xi为b的质因数
我们只需知道把n!分解成 x i y i ′ x_i^{y'_i} xiyi′时最小 y i ′ / y i y'_i/y_i yi′/yi的即可,它就是后缀0的个数
而n!分解成质因数xi可以同上面一样计算n/x,n/x2,n/x3…的个数之和
另外一种思路
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,b;
map<ll,ll>prime(ll n){//把b分解质因数
map<ll,ll>res;
for(ll i=2;i*i<=n;i++){
while(n%i==0){
++res[i];
n/=i;
}
}
if(n!=1)res[n]=1;
return res;
}
ll solve(ll n,ll f){//计算n!分解为质因数f的个数
ll count=0;
while(n){
count+=n/f;
n/=f;
}
return count;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>b;
map<ll,ll>ma=prime(b);
ll mn=0x3f3f3f3f3f3f3f3f;
for(auto it=ma.begin();it!=ma.end();it++){
ll x=solve(n,it->first);
mn=min(mn,x/it->second);
}
cout<<mn<<endl;
}
G. New Year and the Permutation Concatenation
题意
给你一个数n,对于1到n的数,将它所有的排列全部按照字典序排序在拼接在一起形成一个序列,问有多少个长度为n的子序列(连续)有1到n每一个数。
数据范围: 1 ≤ n ≤ 1 0 6 1 \leq n \leq 10^6 1≤n≤106
tags:找规律
思路
打标找规律
可以猜测结果一定为n!+x,问题就是打标找出x的规律
可以看到x为(上一个答案的结果-1*n)
比如:当n=3时,结果为9
这时计算n=4时,n!=24,x=32=n*(9-1)=32
于是就可以递归的求出结果,注意先用数组保存计算的阶乘值,方便O(1)时间查找
代码
打标代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
ll n;
for(int k=1;k<=10;k++){
n=k;
ll a[10000]={0};
for(int i=1;i<=n;i++)a[i]=i;
vector<int>v;
do{
for(int i=1;i<=n;i++)v.push_back(a[i]);
}while(next_permutation(a+1,a+1+n));
// for(int i=0;i<v.size();i++)cout<<v[i]<<' ';
// cout<<endl;
ll res=0;
for(int i=0;i+n-1<v.size();i++){
ll ans=0;
for(int j=i;j<i+n;j++){
ans+=v[j];
}
if(ans==(n*(n+1))/2)res++;
}
ll x=1;
for(int i=1;i<=k;i++)x*=i;
cout<<"k="<<k<<"****k!:"<<x<<"****答案:"<<res<<"****答案-k!:"<<res-x<<endl;
}
}
AC代码
#include<bits/stdc++.h>
#define ll long long
#define mod 998244353
using namespace std;
const int maxn=1e6+5;
ll a[maxn]={1};
ll solve(ll n){
if(n==1)return 1;
else return (a[n]+n*(solve(n-1)-1)%mod)%mod;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
ll n;
cin>>n;
for(int i=1;i<=n;i++)a[i]=(i*a[i-1])%mod;
cout<<solve(n)<<endl;
}
H. Berland Fair
题意
给你n个数围成一个圈,从第一个数开始顺时针遍历,你手上的数为t,若t>=ai你一定会减掉ai,问否则跳过,问最多可以操作几次
数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 , 1 ≤ T ≤ 1 0 18 , 1 ≤ a i ≤ 1 0 9 1 \leq n \leq 2*10^5,1 \leq T \leq 10^{18},1 \leq a_i \leq 10^9 1≤n≤2∗105,1≤T≤1018,1≤ai≤109
思路:思维,暴力
思路
要有技巧的暴力
- 若ai和为sum,第一次直接将k减到sum以下,然后暴力,超时
- 第二次排序在逐轮更新sum并将t减到sum以下,但是理解错了题意,一定要按顺序遍历,不能改变顺序
正确的暴力:就是在上面第2中方法下,不要取排序的就行
- 看剩下的t一轮可以减的数的和sum
- 然后一次计算多轮结果,
res+=t/sum*count;t%=sum;
,直到t比最小的数还小不能进行为止
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5;
ll n,t;
ll a[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>t;
ll res=0,mn=1e9+5;
for(int i=1;i<=n;i++){
cin>>a[i];
mn=min(mn,a[i]);
}
while(t>=mn){
ll sum=0;
ll left=t;
ll count=0;
for(int i=1;i<=n;i++){
if(left>=a[i]){
left-=a[i];
sum+=a[i];
count++;
}
}
res+=t/sum*count;
t%=sum;
}
cout<<res<<endl;
}
复杂度:进似 O ( n l o g n ) O(nlogn) O(nlogn)
I. Multiplicity动规好题
题意
i|bi就是bi能够倍i整除
数据范围: 1 ≤ n ≤ 100000 , 1 ≤ a i ≤ 1 0 6 1 \leq n \leq 100000,1 \leq a_i \leq 10^6 1≤n≤100000,1≤ai≤106
tags:动态规划
思路
- DP抽象意义:
dp[i][j]:前i个数中,长度为j的合法子序列的个数
- 状态转移:对于第i个数,求出它的所有因子,然后从大到小(如果小的先遍历会对后面大的产生影响)遍历所有因子,因子的大小j即为第i个数放的位置(也等同于长度为j的子序列),于是``dp[i][j]=dp[i-1][j-1]+1`,从前i-1个数中长度为j-1的子序列而来
- 边界条件:
dp[0][0]=1
有点难想到这状态转移,更具因子来的
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5,mod=1e9+7;
int n;
int a[maxn];
ll dp[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=0;i<n;i++)cin>>a[i];
dp[0]=1;
for(int i=0;i<n;i++){
vector<int>v;
for(int j=1;j*j<=a[i];j++){
if(a[i]%j==0)v.push_back(j);
if(a[i]%j==0&&j*j!=a[i])v.push_back(a[i]/j);
}
sort(v.begin(),v.end(),greater<int>());
for(int i=0;i<v.size();i++){
dp[v[i]]=(dp[v[i]]+dp[v[i]-1])%mod;
}
}
ll ans=0;
for(int i=1;i<=n;i++)ans=(ans+dp[i])%mod;
cout<<ans<<endl;
}
J. Bicolorings动规好题(状态维)
题意
2行n列的网格,图黑白颜色,问有多少种方法可以是最终有k个连通块
数据大小: 1 ≤ n ≤ 1000 , 1 ≤ k ≤ 2 ∗ n 1 \leq n \leq 1000, 1 \leq k \leq 2*n 1≤n≤1000,1≤k≤2∗n
tags:动态规划
思路
加一个维度表示状态:若为0表示该列为同色,若为1表示该列不同色
-
DP抽象意义
dp[i][j][k]:前i列中有j个连通块,其中第i列状态为k
-
状态转换方程
dp[i][j][0]=(2*dp[i-1][j][1]+dp[i-1][j-1][0]+dp[i-1][j][0])%mod; dp[i][j][1]=(2*dp[i-1][j-1][0]+dp[i-1][j][1]+dp[i-1][j-2][1])%mod
主要是根据第i列和第i-1列的连通块颜色,来改变第前i-1列中所需的连通块数数量以保证加了第i列后连通块数量为j
-
边界条件:
dp[1][2][1]=2,dp[1][1][0]=1
-
结果:
dp[n][k][1]+dp[n][k][0]
,两个状态都要计算
代码
#include<bits/stdc++.h>
#define ll long long
#define mod 998244353
using namespace std;
const int maxn=1e3+1;
int n,k;
ll dp[maxn][2*maxn][2];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>k;
dp[1][2][1]=2;
dp[1][1][0]=2;
for(int i=2;i<=n;i++){
for(int j=1;j<=k;j++){
dp[i][j][0]=(2*dp[i-1][j][1]+dp[i-1][j-1][0]+dp[i-1][j][0])%mod;
dp[i][j][1]=(2*dp[i-1][j-1][0]+dp[i-1][j][1]+dp[i-1][j-2][1])%mod;
}
}
cout<<(dp[n][k][1]+dp[n][k][0])%mod<<endl;
}