这场简单些,E题是个推结论的数学题,沾点高精的思想。F是个需要些预处理的DP,G题是用exgcd算边权的堆优化dijkstra。C题有点骗,硬啃很难做。


A Thorns and Coins

题意:

在你的电脑宇宙之旅中,你偶然发现了一个非常有趣的世界。这是一条有 nnn 个连续单元格的路径,每个单元格可能是空的,也可能包含荆棘或一枚硬币。在一次移动中,你可以沿着这条路径移动一个或两个单元格,前提是目的地单元格不包含荆棘(并且属于这条路径)。如果您移动到有硬币的格子,您就会拾起它。

这里,绿色箭头代表合法移动,红色箭头代表非法移动。

您想要收集尽可能多的硬币。如果你从路径最左边的单元格开始,请找出你在已发现世界中最多可以收集到的硬币数量。

思路:

模拟以下过程即可。一步一步走,如果是金币就捡,如果是地刺就尝试跳一步,下一步还是地刺就结束游戏。

code:

#include #include #include using namespace std;int T,n;string s;int main(){cin>>T;while(T--){cin>>n>>s;int cnt=0;for(int i=0;i<=n;i++){if(s[i]=='@')cnt++;if(s[i]=='.')continue;if(s[i]=='*'){if(s[i+1]!='*')continue;else break;}}cout<<cnt<<endl;}return 0;}

B Chaya Calendar

题意:

查亚部落相信世界末日有 nnn 个征兆。随着时间的推移,人们发现第 iii 个征兆每隔 ai a_iai 年( ai a_iai 年、 2 ⋅ ai 2 \cdot a_i2ai 年、 3 ⋅ ai 3 \cdot a_i3ai 年、 …\dots 年)就会出现。

根据传说,世界末日必须按顺序出现。也就是说,首先要等待第一个征兆出现,然后严格按照这个顺序,第二个征兆才会出现,以此类推。也就是说,如果第 iii 个征兆在 xxx 年出现,那么部落就会从 x + 1x+1x+1 年开始等待第 ( i + 1 )(i+1)(i+1) 个征兆的出现。

哪一年会出现第 nnn 个征兆,即世界末日会在哪一年发生?

思路:

这个题的数据居然没卡 O ( 1 06∗ n )O(10^6*n)O(106n)的做法,导致赛后一大批萌新被叉。

一个比较明显的思路就是模拟一下每个征兆出现的年数,假设算出了前 i − 1i-1i1 个的征兆最后一个征兆发生的年份是 n wnwnw,那么第 iii 年发生征兆的年份是大于 n wnwnw 的最小倍数。推一下式子递推即可。

code:

#include #include using namespace std;int T,n,nw;int main(){cin>>T;while(T--){cin>>n;nw=0;for(int i=1,t;i<=n;i++){cin>>t;nw=(nw/t+1)*t;}cout<<nw<<endl;}return 0;}

C LR-remainders

题意:

给你一个长度为 nnn 的数组 aaa 、一个正整数 mmm 和一串长度为 nnn 的命令。每条命令要么是字符 “L”,要么是字符 “R”。

按照字符串 sss 中的顺序处理所有 nnn 命令。处理一条命令的步骤如下:

  • 首先,输出数组 aa a 中所有元素的乘积除以 mm m 后的余数。
  • 然后,如果命令是 “L”,则从数组 aa a 中删除最左边的元素;如果命令是 “R”,则从数组 aa a 中删除最右边的元素。

注意,每次移动后,数组 aaa 的长度都会减少 111 ,处理完所有命令后,数组 aaa 将为空。

请编写一个程序,按照字符串 sss 中的顺序(从左到右)处理所有命令。

思路:

一般思路是模拟操作的思路,然后算出 现在区间的数的乘积的余数,但是这东西不好算。要么你写高精算前缀积,但是这样要实现高精乘法和高精除高精。要么算出整段区间的乘积对模数的余数,然后用逆元来乘实现模意义下的除法,不过这个思路是错的。因为模数 mmm 不能保证和所有 ai a_iai 互质,欧拉定理用不了,费马小定理更用不了。逆元算不出来。

所以考虑其他思路,发现虽然乘逆元缩减区间实现不了,但是区间操作的过程我们可以反过来,把乘逆元缩减区间变成乘这个数扩展区间。先沿着缩减区间的步骤走到终点,再从终点一步一步走回来,这和递归的思路很像。

考虑递归,先沿着操作递归到终点。然后返回的时候返回区间乘积的余数,顺便记录每个位置的余数,之后顺序输出即可。

code:

#include #include #include #include using namespace std;const int maxn=2e5+5;typedef long long ll;int T,n,m,a[maxn];string op;vector<int> ans;ll print(int x,int l,int r){if(x>=n)return 1;ll tmp;if(op[x]=='L')tmp=a[l]*print(x+1,l+1,r)%m;else tmp=a[r]*print(x+1,l,r-1)%m;//printf("%d ",tmp);ans.push_back(tmp);return tmp;}int main(){cin>>T;while(T--){cin>>n>>m;for(int i=1;i<=n;i++)cin>>a[i];cin>>op;ans.clear();print(0,1,n);for(auto it=ans.rbegin();it!=ans.rend();it++)cout<<*it<<" ";puts("");}return 0;}

D Card Game

题意:

两名玩家正在玩一款在线纸牌游戏。游戏使用一副 32 张牌。每张牌都有花色和等级。共有四种花色:梅花、方块、红心和黑桃。我们将分别用字符 “C”、“D”、”H “和 “S “对它们进行编码。共有 8 个等级,依次递增:‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’.

每张牌都用两个字母表示:等级和花色。例如,红心 8 表示为 8H。

游戏开始时,选择一种花色作为王牌花色

在每一轮游戏中,玩家都要这样出牌:第一位玩家将自己的一张牌放在桌上,第二位玩家必须用自己的一张牌击败这张牌。之后,两张牌都被移至弃牌堆。

如果两张牌的花色相同,且第一张牌的等级高于第二张牌,那么这张牌就能打败另一张牌。例如,8S 可以打败 4S。此外,一张王牌可以无视等级打败任何一张非王牌,比如,如果王牌花色是梅花(“C”),那么 3C 可以击败 9D。请注意,王牌只能被等级更高的王牌击败。

游戏中一共进行了 nnn 轮,因此弃牌堆中现在有 222$ 张牌。你想重建游戏中的回合,但是弃牌堆中的牌是洗过的。请找出游戏中可能出现的 nnn 个回合。

思路:

要注意到两个东西:

  1. 王牌花色可以无视点数打败其他花色的牌
  2. 每种牌只有一张,每张牌互不相同

所以一个很明显的思路就是先顺序输出其他花色,两个两个输出,其他花色剩一张就用王牌花色凑一张,最后输出王牌花色。所以无解的判定条件很明显,就是其他花色中 奇数张的花色 的个数不超过王牌花色牌个数即可。因为题目给的是偶数张牌,所以不需要是否是奇数张牌。

考虑如何输出,不难想到用set存储每张牌,开4个set存储四种花色。不过输出的时候判断每种花色是不是王牌花色比较麻烦。可以输入的时候直接把王牌花色的牌放到单独的set中,不放到4个set对应的花色中,其他花色的牌正常放即可。

code:

#include #include #include #include using namespace std;int T,n;char wp;void pcard(int x,int color=0){printf("%d%c ",x,((color)" />" CDHS"[color]:wp));}int main(){cin>>T;while(T--){cin>>n>>wp;n<<=1;set<int> s[5],t; for(int i=1;i<=n;i++){string tmp;cin>>tmp;if(tmp[1]==wp)t.insert(tmp[0]-'0');else {switch(tmp[1]){case 'C':s[1].insert(tmp[0]-'0');break;case 'D':s[2].insert(tmp[0]-'0');break;case 'H':s[3].insert(tmp[0]-'0');break;case 'S':s[4].insert(tmp[0]-'0');break;}}}if((s[1].size()&1)+(s[2].size()&1)+(s[3].size()&1)+(s[4].size()&1)<=t.size()){for(int i=1;i<=4;i++){while(!s[i].empty()){pcard(*s[i].begin(),i);s[i].erase(s[i].begin());if(s[i].empty()){pcard(*t.begin());t.erase(t.begin());}else {pcard(*s[i].begin(),i);s[i].erase(s[i].begin());}printf("\n");}}while(!t.empty()){pcard(*t.begin());t.erase(t.begin());pcard(*t.begin());t.erase(t.begin());printf("\n");}}else printf("IMPOSSIBLE\n");}return 0;}

E Final Countdown

题意:

你身处一个即将爆炸并摧毁地球的核实验室。您必须在最后倒计时为零之前拯救地球。

倒计时由 nnn ( 1 ≤ n ≤ 4 ⋅ 1 05 1 \le n \le 4 \cdot 10^51n4105 ) 个机械指示器组成,每个指示器显示一位小数。你注意到,当倒计时的状态从 xxx 变为 x − 1x-1x1 时,并不是一蹴而就的。相反,每个数字的变化都需要一秒钟。

因此,举例来说,如果倒计时显示 42,那么它将在一秒钟内变为 41,因为只有一位数发生了变化;但如果倒计时显示 2300,那么它将在三秒钟内变为 2299,因为最后三位数发生了变化。

找出倒计时归零前还剩多少时间。

思路:

考虑到只要有一位退位就会用掉一秒(假设把个位上的数减一也看作退一位),而减一的逆过程就是加一,被减数减 111 退位的次数和减数加 111 产生的进位的次数是相同的,因此我们把一个数不断减一到零产生的退位次数相当于给零加一不断加到这个数产生的进位次数。

而不断加 111,每个数位上会产生多少进位,也就是变化呢。以12345举例,不难发现个位会变化12345次,十位会变化1234次,百位会变化123次,千位变化12次,各位变化1次,总的变化次数就是 12345 + 1234 + 123 + 12 + 1 = 1371512345+1234+123+12+1=1371512345+1234+123+12+1=13715 次。

不过直接写高精实现高精加法的话,由于 n = 4 ∗ 1 05 n=4*10^5n=4105,一个高精数是 nnn 位, nnn 个高精数相加的复杂度是 n2 n^2n2 的。会TLE。

把式子写成竖式的形式,就会发现一个规律:

12345 1234123 121-----13715

居然!由于是对称的,个位上的数相当于前五个 数位上每个数的和 模10(多出来的数进位了),十位上的数相当于前四位上每个数的和加上来自个位的进位 模10,同理百千万位上的数。

这提示我们可以用前缀和算出每个数位上的数,最后处理一下进位即可,前缀和 和 处理进位的时间复杂度是 O ( n )O(n)O(n) 的,而前缀和不会超 9 ∗ 4 ∗ 1 05 9*4*10^594105,不会爆int。

code:

幽默高精度(没有用到就不用看了,放在这里纯纯纪念意义)

#include #include #include #include #include using namespace std;int T,n;struct bigint{vector<int> val;bigint(int x=0){do{val.push_back(x%10);x/=10;}while(x);}bigint(string s){for(auto it=s.rbegin();it!=s.rend();it++)val.push_back(*it-'0');while(val.size()>1 && val.back()==0)val.pop_back();}bigint(vector<int> a){val=a;}void print(){for(auto it=val.rbegin();it!=val.rend();it++)cout<<*it;puts("");}bool eq0(){return val.size()==1 && val[0]==0;}friend bigint operator+(bigint a,bigint b);friend bigint operator>>(bigint a,int x);};bigint operator+(bigint a,bigint b){vector<int> c;int n=max(a.val.size(),b.val.size());for(int i=0;i<n;i++)c.push_back(((a.val.size()>i)?a.val[i]:0)+((b.val.size()>i)?b.val[i]:0));int ct=0;for(int i=0;i<c.size();i++){c[i]+=ct;ct=c[i]/10;c[i]%=10;}while(ct){c.push_back(ct%10);ct/=10;}return bigint(c);}bigint operator>>(bigint a,int x){if(a.val.size()<=x)return bigint(0);else {vector<int> b;for(int i=x;i<a.val.size();i++)b.push_back(a.val[i]);return bigint(b);}return a;}int main(){cin>>T;while(T--){cin>>n;string tmp;cin>>tmp;vector<int> s(n);for(int i=0;i<n;i++){s[i]=((i>0)?s[i-1]:0)+tmp[i]-'0';}reverse(s.begin(),s.end());int ct=0;for(int i=0;i<s.size();i++){s[i]+=ct;ct=s[i]/10;s[i]%=10;}while(ct){s.push_back(ct%10);ct/=10;}while(s.size()>1 && s.back()==0)s.pop_back();//去除前导零for(auto it=s.rbegin();it!=s.rend();it++)cout<<*it;puts("");}return 0;}

F Feed Cats

题意:

在这个有趣的游戏中,您需要喂养来来往往的猫咪。游戏的关卡由 nnn 步组成。有 mmm 只猫; iii 只猫出现在 li l_ili ri r_iri (包括 ri r_iri )。在每一步中,您可以喂养当前出现的所有猫咪,或者什么也不做。

如果您喂同一只猫超过一次,它就会暴饮暴食,您就会立即输掉游戏。您的目标是在不导致任何一只猫暴食的情况下喂食尽可能多的猫。

找出您能喂养的最大猫咪数量。

从形式上看,您需要从 111 nnn 的线段中选择几个整数点,使得在给定的线段中,没有一个线段覆盖两个或两个以上所选的点,并且有尽可能多的线段覆盖到点。

思路:

如果我们从左到右走,要喂一个点上的猫,那么一定不能和前面的冲突,否则就会喂死,所以我们枚举包含这个这个点的所有猫 薛定谔的出现 区间,找到最小的左区间,在次之前喂猫就没有问题了。

假设我们现在尝试喂点 iii 处的猫,最小左端点是 lll,如果 lll 之前有一种喂法能使得喂到的猫最多,那么这个喂法加上点 iii 处的猫的个数就是 喂点 iii 处的猫,使得喂到的猫最多的喂法。DP的思路就比较显然了。

d p [ i ]dp[i]dp[i] 表示喂点 iii 处的猫,使得喂到的猫最多的喂法,还需要维护前缀最大值 p m x [ i ]pmx[i]pmx[i] 表示前 iii 个位置的最大 d pdpdp 值,点 iii 处的猫的个数是 n u m [ i ]num[i]num[i]。转移方程就是 d p [ i ] = p m x [ i − l ] + n u m [ i ]dp[i]=pmx[i-l]+num[i]dp[i]=pmx[il]+num[i]

发现暴力找 lll 太慢了,考虑优化。最直观的想法就是模拟一下从前往后走的过程,用set维护一下当前位置的猫。具体来说使用两个set<pair>,第一个set第一维存储猫的左端点,第二维存储右端点,第二个set反过来,第一维存储右端点,第二维存储左端点。

每次向右移动一位,查询第二个set,把右端点小于当前位置的猫都删掉,同步删掉第一个set中的猫。然后将所有左端点正好是这个位置的猫加入两个set进来,这里可以预先对猫区间按左端点排序,这样就不用遍历猫区间来加入猫了,用个指针边加边移动即可。之后最小左端点看第一个set,猫的个数直接查询set的大小即可。

代码片段如下:
因为猫区间是可以重复的,所以需要多加一维存储没什么用的信息,来区分每个元素。

sort(cat+1,cat+m+1);//猫区间set<pair<pair<int,int>,int> > s1,s2;for(int pos=1,i=1;pos<=n;pos++){while(!s2.empty() && s2.begin()->first.first<pos){pair<pair<int,int>,int> x=*s2.begin();s1.erase(make_pair(make_pair(x.first.second,x.first.first),x.second));s2.erase(x);}while(i<=m && cat[i].first<=pos){s1.insert(make_pair(cat[i],i));s2.insert(make_pair(make_pair(cat[i].second,cat[i].first),i));i++; }pre[pos]=(s1.size())?s1.begin()->first.first:pos;//最小左区间num[pos]=s1.size();//猫个数}

实际上大佬的写法更为简洁(没完全看懂):

模拟一下大佬的, n u mnumnum 数组使用差分思想来实现,然后用指针 ppp 来表示位置。

sort(cat+1,cat+m+1);for(int i=1;i<=n;i++)num[i]=0;for(int i=1,p=1,l,r;i<=m;i++){l=cat[i].first;r=cat[i].second;num[l]++;num[r+1]--;for(;p<l;p++)pre[p]=p;for(;p<=r;p++)pre[p]=l;}

还有一种更巧妙的 O ( n )O(n)O(n) 的写法:指路

这里相当于直接桶排了。

code:

#include #include #include #include using namespace std;const int maxn=1e6+5;const int maxm=2e5+5;typedef long long ll;int T,n,m;pair<int,int> cat[maxm];int pre[maxn],num[maxn];//i点所在猫占领区间左端点 i点猫个数int dp[maxn],pmx[maxn];int main(){cin>>T;while(T--){cin>>n>>m;for(int i=1;i<=m;i++)cin>>cat[i].first>>cat[i].second;sort(cat+1,cat+m+1);for(int i=1;i<=n;i++)num[i]=0;for(int i=1,p=1,l,r;i<=m;i++){l=cat[i].first;r=cat[i].second;num[l]++;num[r+1]--;for(;p<l;p++)pre[p]=p;for(;p<=r;p++)pre[p]=l;}for(int i=1;i<=n;i++){num[i]+=num[i-1];dp[i]=pmx[pre[i]-1]+num[i];pmx[i]=max(pmx[i-1],dp[i]);}cout<<pmx[n]<<endl;}return 0;}

G Moving Platforms

题意:

有一个游戏,你需要穿过一个迷宫。迷宫由 nnn 个平台组成,由 mmm 条通道连接。

每个平台都处于某个级别 li l_ili ,是一个从 000 H − 1H – 1H1 的整数。在一个步骤中,如果你目前在平台 iii 上,你可以留在上面,或者移动到另一个平台 jjj 上。要移动到平台 jjj ,它们必须通过通道相连,而且它们的级别必须相同,即 li= lj l_i = l_jli=lj

每走一步,所有平台的级别都会发生变化。对于所有的 iii ,平台 iii 的新水平面计算为 li′= ( li+ si) m o d   Hl’_i = (l_i + s_i) \bmod Hli=(li+si)modH

你从平台 111 开始。求到达平台 nnn 所需的最少步数。

思路:

只有两个点 u , vu,vu,v 的级别相同的时候才能走它们相连的边,所以可以列出式子,假如在时间 xxx 时两个点级别相同,则有: l u+(x−1)∗ s u≡ l v+(x−1)∗ s v( m o d H)l_u+(x-1)*s_u\equiv l_v+(x-1)*s_v\pmod H lu+(x1)sulv+(x1)sv(modH)x∗( s u− s v)≡ l v− l u+ s u− s v( m o d H)x*(s_u-s_v)\equiv l_v-l_u+s_u-s_v\pmod H x(susv)lvlu+susv(modH)
如果我们令 a = su− sv, c = lv− lu+ su− sv a=s_u-s_v,c=l_v-l_u+s_u-s_va=susv,c=lvlu+susv,那么式子就转化为了 a∗x≡c( m o d H)a*x\equiv c\pmod H axc(modH)
这是一个一次同余式子,使用exgcd(拓展欧几里得定理)来求解。

求解得到 d = g c d ( a , H )d=gcd(a,H)d=gcd(a,H) x′ x’x,令 x0= c / d ∗ x′ x_0=c/d*x’x0=c/dx ,则 x0 x_0x0 a ∗ x ≡ c ( modH )a*x\equiv c\pmod Haxc(modH) 的一个特解,令 t = H / dt=H/dt=H/d,则 x = x0+ k ∗ t ( k 为整数 )x=x_0+k*t\quad(k为整数)x=x0+kt(k为整数) 为上式的通解。

假设现在时间已经过去了 t mtmtm,我们要找最近的一次两个水平面相同,就是要求得 x= x 0+k∗t>tmx=x_0+k*t\gt tm x=x0+kt>tmk> tm− x 0tk\gt \frac{tm-x_0}{t} k>ttmx0∵k为整数\because k为整数 k为整数∴k= ⌊t m − x0 t⌋+1\therefore k=\left\lfloor\frac{tm-x_0}{t}\right\rfloor+1 k=ttmx0+1 不过要实现下取整的时候不能直接使用C++的整数除法,因为这个是向 000 取整的,所以在 t m < x0 tm\lt x_0tm<x0 时反而相当于是上取整。而且我们不能保证 t m ≥ x0 tm\ge x_0tmx0

实际上 x = x0+ k ∗ tx=x_0+k*tx=x0+kt 我们可以看作是模 ttt 同余 x0 x_0x0 的一系列数,所以我们可以对 x0 x_0x0 取模,得到的数仍然是通解, ttt 是正的, t mtmtm 也是正的,所以 x = x0% t − t < t mx=x_0\%t-t\lt tmx=x0%tt<tm,用这个 xxx 就没问题了。

这样就算出来了下一次两个点级别相同是什么时候,这东西就相当于一个边权,我们直接跑最短路即可。

因为是个边权一样的东西,所以可以对每个边预处理exgcd的特解与通解的变化量,然后要用某个边的时候就再用当前的时间来算。

code:

#include #include #include #include #include #include #include using namespace std;typedef long long ll;const int maxn=1e5+5;const ll inf=1e18;ll T,n,m,mod;ll l[maxn],s[maxn];int head[maxn],cnt;struct edge{int v,nxt;}e[maxn<<1];void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=head[u];head[u]=cnt;}ll exgcd(ll a,ll b,ll &x,ll &y){if(!b){x=1;y=0;return a;}ll d=exgcd(b,a%b,x,y);ll z=x;x=y;y=z-y*(a/b);return d;}ll f(int u,int v,ll tm){ll a=((s[u]-s[v])%mod+mod)%mod,b=mod,c=((l[v]-l[u]+s[u]-s[v])%mod+mod)%mod;ll x,y;ll d=exgcd(a,b,x,y);if(c%d)return -1;x=c/d*x;ll t=b/d;x=x%t-t;ll k=(tm-x)/t+1;return x+k*t;}ll f2(int u,int v,ll tm){//暴力做法,用于对拍set<ll> S;for(ll t=tm+1,ans;;t++){ans=(((t-1)*(s[u]-s[v])+(l[u]-l[v]))%mod+mod)%mod;if(ans==0)return t;if(S.count(ans))return -1;else S.insert(ans);}}int main(){cin>>T;while(T--){cin>>n>>m>>mod;for(int i=1;i<=n;i++)cin>>l[i];for(int i=1;i<=n;i++)cin>>s[i];for(int i=1;i<=n;i++)head[i]=0;cnt=0;for(int i=1,u,v;i<=m;i++){cin>>u>>v;add(u,v);add(v,u);}vector<ll> d(n+1,inf);vector<bool> vis(n+1,false);d[1]=0;priority_queue<pair<ll,int>,vector<pair<ll,int> >,greater<pair<ll,int> > > h;h.push(make_pair(d[1],1));while(!h.empty()){int u=h.top().second;if(u==n)break;h.pop();if(!vis[u])vis[u]=true;else continue;for(int i=head[u],v;i;i=e[i].nxt){v=e[i].v;ll tm=f(u,v,d[u]);if(~tm && tm<d[v]){d[v]=tm;h.push(make_pair(d[v],v));}}}if(d[n]!=inf)cout<<d[n]<<endl;else cout<<-1<<endl;}return 0;}