A Yet Another Remainder
题目链接:PTA | 程序设计类实验辅助教学平台

样例输入:
- 1
- 9
- 47
- 27 20
- 14 18 15
- 16 13 11 7
- 13 12 13 5 4
- 12 14 11 2 4 4
- 14 12 8 2 4 4 3
- 12 9 8 2 4 4 3 5
- 9 9 8 2 4 4 3 5 3
- 5
- 3
- 7
- 31
- 67
- 97
样例输出:
- 2
- 1
- 23
- 30
- 87
题意:给定一个n位数,然后给定一个二维数组a[][],a[i][j]代表从第j位(高位开始的第j位)开始每隔i位加上该位数字和所得的一个数位和,i>=j,且给定的二维数组大小不能超过100,对于每次询问给定一个p,输出这个n位数对p取余的结果。
分析:我一开始一直没搞明白p是一个质数且不等于5是什么意思,后来看了题解发现是用费马小定理来做的,但是我在考试时想到的思路是不需要这个限制的,下面来说一下我在考场上想出来的思路:
由于p是小于100的,那么对p取余的结果就是0~99之间的一个,那么假设P[i]表示10^i对p取余的结果,然后我们能够知道,一定存在一个最小周期k,满足P[i]%p=P[i+k]%p,而且k<100,因为P[i+1]=P[i]*10%p,也就是说每次得到下一个值的方法都是固定的,所以一定会存在循环,而P[i]代表的就是第i+1位的权重,那么对于每一些%p相同的位我们直接求其系数和然后乘以其对应的权重对p取余的结果即可知道这些位对p取余的结果,而这样的系数和正是题目中所给出的那种形式,所以我们处理出来所有的情况然后相加对p取余即可。
细节见代码:
- #include<cstdio>
- #include<iostream>
- #include<algorithm>
- #include<cstring>
- #include<map>
- #include<queue>
- #include<vector>
- #include<cmath>
- using namespace std;
- const int N=103;
- int a[N][N];
- int P[N];//P[i]记录10^i对p取余得到的结果
- int main()
- {
- int T;
- cin>>T;
- while(T--)
- {
- int n;
- scanf("%d",&n);
- for(int i=1;i<=min(100,n);i++)
- for(int j=1;j<=i;j++)
- scanf("%d",&a[i][j]);
- int q;
- scanf("%d",&q);
- while(q--)
- {
- int p;
- scanf("%d",&p);
- P[0]=1;
- int id=1;
- for(;id<=100;id++)
- {
- P[id]=(P[id-1]*10)%p;
- if(P[id]==P[0]) break;
- }
- int ans=0;
- for(int i=1;i<=min(id,n);i++)
- ans=(ans+1ll*a[min(id,n)][i]*P[(n-i)%id])%p;
- printf("%d\n",ans);
- }
- }
- return 0;
- }
B Non-decreasing Array

样例输入:
- 5
- 1 2 3 4 5
样例输出:
- 10
- 16
- 16
- 16
- 16
给定一个长度为n的非递减数组,我们可以对其进行k次操作,每次操作可以选择一个数删除,然后再选择一个数将其的值变成任意值,但是要保证每次操作后的数组还是一个非递减数组,而且删除或者修改的数不能是数组的开头或者结尾。让我们求对于所有的操作次数在1~k的情况下的最大值,其中m是最后数组中剩余的元素个数。
分析:数据范围是100,比较小,通过简单模拟我们能够发现让这些数两两之间相差尽可能地大是我们想要得到的最优解,而且能够发现删除一个数和修改一个数为任意值是等价的,为什么这么说呢?首先我们要操作的数的区间不包括开头和结尾,也就是我们操作的数后面一定还有数,那么我们删除了这个数和把这个数变成其后面的数就没有什么区别了,因为两两之间的差值不变(两个相等的数之间默认为没有差值),所以操作k次的问题就转化为我们删除不超过2*k个数的情况下所能得到的最大值,这个直接用动态规划做就行,枚举倒数第二个数然后直接三重for循环枚举即可。
细节见代码:
- #include<cstdio>
- #include<iostream>
- #include<algorithm>
- #include<cstring>
- #include<map>
- #include<queue>
- #include<vector>
- #include<cmath>
- using namespace std;
- const int N=1e2+10;
- long long f[N][N];//f[i][j]表示前i个数删除了j个数且保留了第i个数的最大值
- long long a[N];
- int main()
- {
- int n;
- cin>>n;
- for(int i=1;i<=n;i++)
- scanf("%lld",&a[i]);
- f[2][0]=(a[2]-a[1])*(a[2]-a[1]);
- for(int i=2;i<=n;i++)//当前遍历到的数
- for(int j=1;j<i;j++)//所要保留的倒数第二个数
- for(int k=i-j-1;k<n-1;k++)//枚举删的次数
- f[i][k]=max(f[i][k],f[j][k-(i-j-1)]+(a[i]-a[j])*(a[i]-a[j]));
- int cnt=n;
- for(int i=2;i<=n-2;i+=2)
- printf("%lld\n",f[n][i]),cnt--;
- while(cnt--)
- printf("%lld\n",(a[n]-a[1])*(a[n]-a[1]));
- return 0;
- }
E An Interesting Sequence

样例输入:
2 5
样例输出:
7
题意:给定一个长度为n的数组的第一个元素值,让我们构造一个数组使得整个数组的元素和最小且每两个相邻的数的最大公约数是1(所有元素值均大于1).
分析:直观想象就是通过2,3,2,3,……这样的方法进行构造,这样显然是最优的,我们只需要找到与给定的第一个数互质的最小的数,然后分类讨论第三个数枚举2还是3即可。
- #include<cstdio>
- #include<iostream>
- #include<algorithm>
- #include<cstring>
- #include<map>
- #include<queue>
- #include<vector>
- #include<cmath>
- using namespace std;
- int main()
- {
- int n,k;
- cin>>n>>k;
- int id=2;
- while(__gcd(id,k)!=1) id++;
- int ans=k+id;
- if(id&1)
- {
- for(int i=3;i<=n;i++)
- if(i&1) ans+=2;
- else ans+=3;
- }
- else
- {
- for(int i=3;i<=n;i++)
- if(i&1) ans+=3;
- else ans+=2;
- }
- printf("%d",ans);
- return 0;
- }
F Infinity Tree

样例输入:
- 3
- 2 6 7
- 2 4 5
- 3 20 2
样例输出:
- 2
- 1
- 2
题意:一开始有一个节点1,然后每秒每个节点产生k个节点,节点编号依次递增,一轮之后会有k+1个节点,然后下一轮次每个节点再产生k个节点,编号小的节点先产生。接下来有若干询问,对于每组询问,给定两个节点u和v,求lca(u,v)
分析:容易发现每一次扩展后总结点的个数就是k+1的一个幂次,也就是第i次扩展后的节点总个数就是(k+1)^i,那么我们很容易求得第i次扩展得到的节点编号就是(k+1)^(i-1)+1~(k+1)^i,那么我们很容易求得位于该层的节点的父节点是哪个,直接除以k上取整即可,于是我们可以类似于求解lca的那种暴力跳的方法,每次选择一个节点编号大的节点找其父亲节点直至两个节点相同。注意可能会爆long long,建议直接开__int128
细节见代码:
- #include<cstdio>
- #include<iostream>
- #include<algorithm>
- #include<cstring>
- #include<map>
- #include<queue>
- #include<vector>
- #include<cmath>
- using namespace std;
- const int N=103;
- __int128 p[N];//存储k+1的幂次
- __int128 k;
- long long x,y;
- int mx;
- __int128 qpow(__int128 a,int b)
- {
- __int128 ans=1;
- while(b)
- {
- if(b&1) ans*=a;
- b>>=1;
- a*=a;
- }
- return ans;
- }
- int log(long long x)//返回log(k+1)(x)的上取整
- {
- int l=0,r=mx;
- while(l<r)
- {
- int mid=l+r>>1;
- if((long long)p[mid]>=x) r=mid;
- else l=mid+1;
- }
- return l;
- }
- int main()
- {
- int T;
- cin>>T;
- while(T--)
- {
- long long t;
- scanf("%lld%lld%lld",&t,&x,&y);
- k=t;
- if(x==1||y==1)
- {
- puts("1");
- continue;
- }
- p[0]=1;
- for(int i=1;;i++)
- {
- p[i]=p[i-1]*(k+1);
- if(p[i]>1e18)
- {
- mx=i;
- break;
- }
- }
- while(x!=y)
- {
- if(x<y) swap(x,y);
- int c=log(x);//找到x节点的层
- long long id=qpow(k+1,c-1)+1;//找到x节点所在层的第一个节点的编号
- x=ceil(1.0*(x-id+1)/k);
- }
- printf("%lld\n",x);
- }
- return 0;
- }
G Good Permutation

样例输入:
- 5 3
- 1 5
- 1 4
- 1 3
样例输出:
24
题意:现在有一个长度为n的1~n的全排列,定义![]()
定义一个区间是好区间当且仅当这个区间中的最大值减去最小值就是区间长度减1,现在给定若干个限制,每个限制以一个区间[l,r]的形式给出,代表这个区间是好区间,保证所有的限制是不相交的,问满足这样性质的排列个数。
分析:先来给出好区间的另一个通俗定义,就是这个区间内的数是连续的,这是显然的,假如在区间[l,r]内有n个不相互包含的限制,那么对于这种情况下的方案数就是每个区间长度的全排列数目相乘再把每个区间看成一个点然后和所有的孤立点一起进行一次全排列的方案数乘积,这是显然的,所以我们就转化为求出所有的不包含区间的情况,因为对于每一个区间我们都可以直接算出其内不包含子区间的方案数,然后用树形DP直接求解即可,每个区间的父节点就是第一个包含这个区间的区间,那么现在的问题就转化为如何建树,我们先对所有的区间按照左区间为第一关键字从小到大排序,右区间为第二关键字从大到小排序,然后现在对于每个区间我们都找他左边的且第一个右区间比当前区间右区间大的区间,这个可以用单调栈解决。
细节见代码:
- #include<cstdio>
- #include<iostream>
- #include<algorithm>
- #include<cstring>
- #include<map>
- #include<queue>
- #include<vector>
- #include<cmath>
- using namespace std;
- const int N=1e6+10,mod=1e9+7;
- long long fac[N];
- long long f[N];//f[i]代表第i个区间的贡献
- int len[N];//len[i]记录第i个区间的长度
- vector<int>p[N];
- struct node
- {
- int id,l,r;
- };
- bool cmp(node a,node b)
- {
- if(a.l!=b.l) return a.l<b.l;
- return a.r>b.r;
- }
- vector<node>q;
- int n,m;
- void dfs(int x)
- {
- long long ans=1;
- long long l=0;
- for(int i=0;i<p[x].size();i++)
- {
- int j=p[x][i];
- dfs(j);
- ans=ans*f[j]%mod;
- l+=len[j];
- }
- f[x]=ans*fac[len[x]-l+p[x].size()]%mod;
- }
- int st[N],tt;//用单调栈求左边第一个大于当前区间右边界r的区间编号
- int main()
- {
- cin>>n>>m;
- fac[0]=1;
- for(int i=1;i<=n;i++)
- fac[i]=fac[i-1]*i%mod;
- q.push_back(node{0,1,n});
- len[0]=n;
- for(int i=1;i<=m;i++)
- {
- int l,r;
- scanf("%d%d",&l,&r);
- q.push_back(node{i,l,r});
- len[i]=r-l+1;
- }
- sort(q.begin(),q.end(),cmp);
- st[++tt]=0;
- for(int i=1;i<=m;i++)
- {
- while(q[st[tt]].r<q[i].r) tt--;
- int fa=q[st[tt]].id;
- st[++tt]=i;
- p[fa].push_back(q[i].id);
- }
- dfs(0);
- printf("%lld",f[0]);
- return 0;
- }
J A Game about Increasing Sequences

样例输入:
- 3
- 1 3 2
样例输出:
Bob
题意:Alice和Bob进行游戏,给定n个数排成一排,Bob和Alice轮流操作,每次操作可以从最左边或者最右边取一个数,前提是当前取的数要大于上一次另一人取的数,谁最后不能操作谁输,让我们输出获胜者。
分析:通过简单模拟我们可以发现其实我们原来的数组并不是全部都是有用的,比如第一个数是5,第二个数是3,那么只有第一个数是有用的,为什么这么说呢?因为我们取的数一定是递增分布的而且要按照顺序取,取3之前必须取5,但是取了5就没办法取3,所以我们可以从前往后预处理出来单调递增序列的长度,同理从后往前也需要预处理出来。这两个长度不妨记为len1和len2.
那么就有如下结论:只有当len1和len2同为偶数时才会有Bob获胜否则一定为Alice获胜。
我们先来看一下偶数和偶数的情况吧:Alice选择一个数,然后Bob可以在相同的一端选择Alice后面的那个数,如果Alice换了方向,那么Bob也跟着换方向,所以Bob必胜。
如果要是奇数+偶数的情况,Alice取奇数端的第一个数,如果这个数大于等于偶数端的第一个数,那么顺序就固定了,Alice必胜,否则我们可以发现当Alice取完第一个奇数后变成了偶数+偶数的情况,这个时候Bob取哪个数Alice就跟着即可,所以Alice必胜。
偶数+奇数的情况同理
最后一种情况就是奇数+奇数的情况,那么Alice选择两端较大的那个数,那么这个顺序就固定了,Alice直接赢。
- #include<cstdio>
- #include<iostream>
- #include<algorithm>
- #include<cstring>
- #include<map>
- #include<queue>
- #include<vector>
- #include<cmath>
- using namespace std;
- const int N=1e5+10;
- int a[N];
- int main()
- {
- int n;
- cin>>n;
- for(int i=1;i<=n;i++)
- scanf("%d",&a[i]);
- int lmid=1,rmid=n;
- while(lmid<n&&a[lmid]<a[lmid+1]) lmid++;
- while(rmid>1&&a[rmid]<a[rmid-1]) rmid--;
- if((lmid%2==0)&&(n-rmid+1)%2==0) puts("Bob");
- else puts("Alice");
- return 0;
- }