GJOI 10.4/10.5 题解
这场直接成为骗分大师。不过 T1 还是太险了,计数题、容斥题还是要去多模拟。
1.AT_abc304_f Shift Table
题意
A 和 B 将在接下来的 NNN 天里做兼职。A 的排班表由字符串 AAA 给出,AAA 的第 iii 个字符为 #
时表示第 iii 天上班,为 .
时表示第 iii 天不上班。
基于此,B 按照如下方式制作了自己的排班表:
- 首先,取 NNN 的一个正因数 MMM,但 M≠NM \neq NM=N。
- 接着,决定第 111 天到第 MMM 天的出勤情况。
- 最后,依次对 i=1,2,…,N−Mi = 1, 2, \ldots, N - Mi=1,2,…,N−M,令第 M+iM + iM+i 天的出勤情况与第 iii 天相同。
需要注意的是,即使 MMM 的取值不同,最终得到的排班表也可能相同。
请计算,在 NNN 天中,每一天 A 和 B 至少有一人上班的情况下,B 的排班表可能有多少种?
结果对 998244353998244353998244353 取模。
2≤N≤1052\le N\le 10^52≤N≤105。
思路
赛时差点没做出来。还是收到大佬的点化。所以还是要加强写代码实践能力啊!想清楚那个容斥关系之后其实并不难。
对于原串 AAA,我们找出其所有 .
的位置,这些位置(天数)B 必须干活(填 #
)。而其余位置 222 种字符均可。
于是考虑一个长度为 xxx 的循环天数,容易 O(n)O(n)O(n) 遍历整个 AAA 计算,有多少天 B 必须干活,设有 cntcntcnt 天 B 不是必须干活,那么就有 2cnt2^{cnt}2cnt 种方案。
但是题目已经明示我们,不同的循环天数可能产生相同的排班表,要考虑去重。于是想要找到哪些天数之间,会出现重复。其实这个性质赛时被我猜到了,举一个简单的例子吧:
12
#.#.###.#..#
手玩一下会发现(?
表示不必须填 #
,#
表示必须填 #
):
- x=1x=1x=1,循环子串为
#
; - x=2x=2x=2,循环子串为
##
; - x=3x=3x=3,循环子串为
##?
; - x=4x=4x=4,循环子串为
?###
; - x=6x=6x=6,循环子串为
?#?##?
。
互为倍数的 xxx 的子串是是包含关系。譬如 x=3x=3x=3 对应的两种方案 ############
和 ##.##.##.##.
,在 x=6x=6x=6 可以分别令全部 ?
成为 #
、只令第 111 个 ?
成为 #
,就包含了 x=3x=3x=3 所有情况。
因为同样能够通过复制来满足条件的循环子串,xxx 越大,?
多时可以指代不同字符,从而覆盖小因数的情况。
因此记 fxf_xfx 表示循环天数为 xxx 时,其因数已经占用的方案数。这相当于一个 tag,上文计算完 cntcntcnt 之后,对答案新增贡献就是 tem=2cnt−fxtem=2^{cnt}-f_xtem=2cnt−fx。
那么 fff 怎么更新呢?我们计算完一个小因数 x0x_0x0 的答案 tem0tem_0tem0 后,就可以更新其倍数的 fff 了,即:
fix0←tem0f_{ix_0}\leftarrow tem_0fix0←tem0
如果一个数存在多个因数,且因数又是某个因数的因数,不会算重吗?其实并不会,因为 temtemtem 是新增贡献,并不是包含其因数在内的所有方案数。
时间复杂度 O(nd(n))O(nd(n))O(nd(n)),d(n)d(n)d(n) 为因数个数看作 n\sqrt{n}n。
还是很有意思的一道题。具体细节见代码。记得勤取模。
代码
#pragma GCC optimise(2)
#pragma GCC optimise(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e5+9,mod=998244353,M=329;
ll n;
ll a[N],pos[N];
ll ys[M],m;
ll pw[N];
void init()
{pw[0]=1;for(int i=1;i<N;i++)pw[i]=pw[i-1]*2%mod;//预处理2的幂,少一个log
}
bool vis[N];
ll f[N];
ll cal(ll x)
{for(int i=1;i<=x;i++)vis[i]=0;ll cnt=0;for(int i=1;i<=n;i++){pos[i]=i%x;if(pos[i]==0)pos[i]=x;//每个下标对应的循环天数位置if(!a[i])vis[pos[i]]=1;//必须填#}for(int i=1;i<=x;i++)cnt+=(!vis[i]);ll tem=(pw[cnt]-f[x]+mod)%mod;//对整体答案新增的贡献for(int i=2;i*x<=n;i++)f[i*x]=(f[i*x]+tem)%mod;return tem;
}
int main()
{freopen("clr.in","r",stdin);freopen("clr.out","w",stdout); init();scanf("%lld",&n);for(int i=1;i<=n;i++){char c;cin>>c;a[i]=(c=='#');}for(int i=1;i*i<=n;i++){if(n%i)continue;ys[++m]=i;if(i*i!=n&&i!=1)ys[++m]=n/i;}sort(ys+1,ys+m+1);ll ans=0;for(int i=1;i<=m;i++)ans=(ans+cal(ys[i]))%mod;printf("%lld",ans);return 0;
}
2.洛谷 P3590 POI2015 三座塔 Three towers / P12765 POI2018三座塔 2 Three towers 2
题意
1≤n≤1061\le n\le 10^61≤n≤106。
洛谷 P3590:N
、O
、I
分别改为 B
、C
、S
。
洛谷 P12765:同样改字母,取消“要么只有一种字母”的条件。
思路
这题分还是太好骗了,O(n2)O(n^2)O(n2) 可过赛时 sub1235 拿到 55pts。然而正解似乎就是在暴力的基础上加一些小小性质优化……
“最长的子串满足左端点在 1∼31\sim 31∼3 或右端点在 n−2∼nn-2\sim nn−2∼n。”出自这个讨论。手玩大样例甚至直接过。
这个结论等价于,对于一个合法解 S0S_0S0,在左右若存在 333 个字符,必然存在包含 S0S_0S0 的更优解。
不妨设 (x,y,0)(x,y,0)(x,y,0) 表示 333 种字符的个数,xxx 表示 N
比 I
多 xxx 个,yyy 表示 O
比 I
多 yyy 个,钦定 x>yx>yx>y。接下来我们要讨论,怎么能使得 S0S_0S0 的长度增加,就可以转移到其他情况:
- 若不存在相邻两项差值为 111,那么随便加 111 个都是合法的。
- 若 (y+1,y,0)(y+1,y,0)(y+1,y,0),且 y>1y>1y>1:
- 如果 S0S_0S0 两边字符不同,那就选不是
O
那个(+1+1+1); - 如果 S0S_0S0 两边字符相同,都不是
O
就随便选一个(+1+1+1),都是O
就两个全选(+2+2+2)。
- 如果 S0S_0S0 两边字符不同,那就选不是
- 若 (x,1,0)(x,1,0)(x,1,0),且 x>2x>2x>2:
- 与上大致相同。
- 若 (2,1,0)(2,1,0)(2,1,0)(最极端情况):
??N(S0)I??
:不选I
那边;??O(S0)I??
:此时无论选左还是右,都至少有一对数相同。- 选
O
一侧 (2,2,0)(2,2,0)(2,2,0),出个N
、O
就有解了; - 如果不幸
IIO(S0)I??
,那就先考虑选I
一侧 (2,1,1)(2,1,1)(2,1,1)。??
只带一个N
就全选N?
;- 如果
??=NN
,那就选NN
和左边的O
,O(S0)INN
,(4,3,1)(4,3,1)(4,3,1); - 如果
??=OO/II
也是直接全选 (2,3,1)(2,3,1)(2,3,1) 或 (2,1,3)(2,1,3)(2,1,3)。 - 如果
??=OI
,那就选第 111 个问号的O
和左边的O
,O(S0)IO
,(2,3,1)(2,3,1)(2,3,1); - 如果
??=IO
,那就直接全选IIO(S0)IIO
,(2,3,4)(2,3,4)(2,3,4)。
- 选
??I(S0)I??
:变成 (2,1,2)(2,1,2)(2,1,2),就是差不多的讨论了。
至此,我们简要证明了这个性质的正确性。代码就很好写了。时间复杂度 O(3n)O(3n)O(3n)。
代码
#pragma GCC optimise(2)
#pragma GCC optimise(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e6+9;
ll n;
char c[N];
ll sn[N],so[N],si[N];
bool check(ll l,ll r)
{ll ns=sn[r]-sn[l-1];ll os=so[r]-so[l-1];ll is=si[r]-si[l-1];ll cnt0=(ns==0)+(os==0)+(is==0);if(cnt0==2)return 1;if(ns!=os&&os!=is&&ns!=is)return 1;return 0;
}
int main()
{freopen("str.in","r",stdin);freopen("str.out","w",stdout);scanf("%lld",&n);for(int i=1;i<=n;i++){cin>>c[i];sn[i]=sn[i-1]+(c[i]=='N');so[i]=so[i-1]+(c[i]=='O');si[i]=si[i-1]+(c[i]=='I');}if(sn[n]==0)//sub5 {if(so[n]!=si[n])printf("%lld",n);else printf("%lld",n-1);return 0; }ll ans=0;for(ll i=1;i<=3;i++)for(ll j=n;j>=i;j--)if(check(i,j))ans=max(ans,j-i+1);for(ll j=n-2;j<=n;j++)for(ll i=1;i<=j;i++)if(check(i,j))ans=max(ans,j-i+1);printf("%lld",ans);return 0;
}
3.洛谷 P11346 KTSC2023 会议室 2
4.Baekjoon 21089 Excluded Min
题意
1≤n,q≤5×1051\le n,q\le 5\times 10^51≤n,q≤5×105,ai∈[0,5×105]a_i\in[0,5\times 10^5]ai∈[0,5×105]。
思路
待补。赛时拼到 52pts 暴力。
接下来是 GJOI 10.5 题解。
1.洛谷 P9975 USACO23DEC Cowntact Tracing 2
题意
Farmer John 有 NNN 头奶牛排成一列。不幸的是,有一种疾病正在传播。
最初,有一些奶牛被感染。每到夜晚,被感染的奶牛会将疾病传播给它左右两边的奶牛(如果这些奶牛存在的话)。一旦奶牛被感染,她就会持续处于感染状态。
经过一些晚上,Farmer John 意识到情况已经失控,因此他对奶牛进行了检测以确定哪些奶牛感染了疾病。请找出最少有多少头奶牛最初可能感染了这种疾病。
1≤N≤3⋅1051 \leq N \leq 3\cdot 10^51≤N≤3⋅105。
思路
传染天数是决定有多少头奶牛最开始被感染的关键,天数越大,初始感染数越小。于是我们要找这个最大感染天数。
处理出每个被感染奶牛的连续段 cic_ici。显然的,最大感染天数也要满足所有连续段不能感染到 0
去。于是最大感染天数,取每个 cic_ici 最大可行感染天数的最小值。
一个连续段可以在中间放一头奶牛(奇数个,偶数个就放一对),最多 ⌊ci−12⌋\left\lfloor \frac{c_i-1}{2} \right\rfloor⌊2ci−1⌋ 天——吗?
我们发现,第 111 头奶牛只会向右传染,同理第 nnn 头奶牛只会向左传染。于是,若 c1c_1c1 包含第 111 头奶牛,最大感染天数是 ci−1c_i-1ci−1,对 cnc_ncn 同理。
ll cal(ll x,ll id)
{if(id==1){if(a[1]==1)return x-1;}if(id==cnt){if(a[n]==1)return x-1;}if(x&1)return (x-1)/2;return (x-2)/2;
}
...
ll day=inf;
for(int i=1;i<=cnt;i++)
day=min(day,cal(c1[i],i));
奶牛数就是 ∑i=1cnt⌈ci2day+1⌉\displaystyle\sum_{i=1}^{cnt}\left\lceil \frac{c_i}{2day+1} \right\rceili=1∑cnt⌈2day+1ci⌉,即以一头奶牛为中心,向两边传染,如果有没被传染的就单独开一头奶牛。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3e5+9,inf=3e14;
ll n;
char c[N];
ll a[N],c1[N],cnt,lc[N],rc[N];
ll cal(ll x,ll id)
{if(id==1){if(a[1]==1)return x-1;}if(id==cnt){if(a[n]==1)return x-1;}if(x&1)return (x-1)/2;return (x-2)/2;
}
ll plc(ll i,ll day)
{ll x=c1[i]/(day*2+1),ys=c1[i]%(day*2+1);if(ys)x++;return x;
}
int main()
{freopen("cow.in","r",stdin);freopen("cow.out","w",stdout);scanf("%lld%s",&n,c+1);for(int i=1;i<=n;i++)a[i]=(c[i]=='1');for(int i=0;i<=n;i++){if(a[i]==1)c1[cnt]++;else if(a[i+1]==1)cnt++;}if(cnt==0){puts("0");return 0; }ll day=inf,ans=0;for(int i=1;i<=cnt;i++)day=min(day,cal(c1[i],i));for(int i=1;i<=cnt;i++)ans+=plc(i,day);printf("%lld",ans);return 0;
}
2.洛谷 P7831 CCO2021 Travelling Merchant
题意
一个国家有 nnn 个城市和 mmm 条单向道路,一个旅行商在这些城市之间旅行。
第 iii 条道路从城市 aia_iai 到城市 bib_ibi,只有当他的资产不少于 rir_iri 元才可以走这条道路,走过这条道路之后他的资产会增加 pip_ipi 元。
他希望自己可以永远不停的游走下去,于是他想知道从任意一个城市出发至少需要多少元初始资产。
2≤n,m≤2×1052 \leq n, m \leq 2 \times 10^52≤n,m≤2×105,1≤ai,bi≤n1 \leq a_i, b_i \leq n1≤ai,bi≤n,ai≠bia_i \neq b_iai=bi,0≤ri,pi≤1090 \leq r_i, p_i \leq 10^90≤ri,pi≤109,保证没有自环但可能有重边。
思路
赛时真是一点头绪都没有捏。
设 ansuans_uansu 表示 uuu 的答案,实则有转移:
ansu←min(r(u,v),ansv−p(u,v))ans_u\leftarrow \min(r(u,v),ans_v-p(u,v))ansu←min(r(u,v),ansv−p(u,v))
于是可以考虑建反图,从 vvv 回溯到 uuu,但是图上会有环怎么办?
其实可以发现一些性质:
- 若初始资产为所有边 rrr 的最大值,就是畅通无阻的;
- 出度为 000 的点无解;
- 对于有向边 u→vu\to vu→v,若 vvv 出度为 000,可以直接删去 u→vu\to vu→v;
- 在能够走到环之前,每条边至多被走一次。
于是可以考虑类似拓扑排序的思想,上面提到建反图,我们就把所有(原图上)出度为 000 的点入队(即反图上 000 入度)。反图上如果 vvv 有解就更新 uuu。
这样我们就可以把所有通向出度为 000 的末梢给扔掉,剩下剥不开的环和指向环的单条路径。
怎么处理这些路径上的贡献呢?考虑第 iii 条边。如果“拓扑”完,iii 边还没被删除,说明出现单调路径。其实可以从大到小考虑,iii 没被删直接讲 ansu←ri(u,v)ans_u\leftarrow r_i(u,v)ansu←ri(u,v)。
还是过于智慧了。本题实现参考了这篇博客。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9,inf=1e18;
ll n,m;
struct bian
{ll u,v,id,lim,val;
}b[N];
bool cmp(bian x,bian y)
{return x.lim>y.lim;
}
struct edge
{ll to,next,id,lim,w;
}e[N<<1];
ll idx,head[N];
void addedge(ll u,ll v,ll id,ll lim,ll val)
{idx++;e[idx].to=v;e[idx].next=head[u];e[idx].id=id;e[idx].lim=lim;e[idx].w=val;head[u]=idx;
}
ll out[N];
ll ans[N];
bool vis[N];
queue<ll>q;
int main()
{freopen("tour.in","r",stdin);freopen("tour.out","w",stdout);scanf("%lld%lld",&n,&m);for(int i=1;i<=m;i++){ll u,v,lim,val;scanf("%lld%lld%lld%lld",&u,&v,&lim,&val);b[i]=(bian){u,v,i,lim,val};addedge(v,u,i,lim,val);out[u]++;}sort(b+1,b+m+1,cmp);for(int i=1;i<=n;i++){ans[i]=inf;if(!out[i])q.push(i);}for(int I=1;I<=m;I++){while(!q.empty()){ll v=q.front();q.pop();for(int i=head[v];i;i=e[i].next){ll id=e[i].id;if(vis[id])continue;vis[id]=1;ll u=e[i].to,lim=e[i].lim,val=e[i].w;if(ans[v]!=inf)ans[u]=min(ans[u],max(lim,ans[v]-val));out[u]--;if(!out[u])q.push(u);}}ll u=b[I].u;if(!vis[b[I].id]){vis[b[I].id]=1;ans[u]=min(ans[u],b[I].lim);out[u]--;if(!out[u])q.push(u);}}for(int i=1;i<=n;i++){if(ans[i]>=inf)printf("-1 ");else printf("%lld ",ans[i]);}return 0;
}