2021暑假校队集训
模拟赛
Day 1-qyc
T1 ARC059F
注意到 \(s\) 串具体是什么对答案没有影响,考虑先求出 \(m\) 步弄出长度为 \(n\) 的串的方案数,然后直接乘上 \(2^n\) 的逆元即可。
点击查看代码
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
const int N=5000+13,mod=1e9+7;
inline int qpow(int a,int k){int s=1;for(;k;k>>=1,a=(ll)a*a%mod)if(k&1)s=(ll)s*a%mod;return s;}
int n,m;
char useless[N];
int f[N][N<<1];
int main(){
freopen("typewriter.in","r",stdin);
freopen("typewriter.out","w",stdout);
scanf("%d%d",&n,&m);
scanf("%s",useless+1);
f[0][0]=1;
for(int i=1;i<=m;++i){
for(int j=0;j<=n+m/2;++j){
if(j) f[i][j]+=f[i-1][j-1]*2%mod,f[i][j]%=mod;
else f[i][j]+=f[i-1][j],f[i][j]%=mod;
f[i][j]+=f[i-1][j+1],f[i][j]%=mod;
}
}
printf("%d\n",(ll)f[m][n]*qpow(qpow(2,n),mod-2)%mod);
return 0;
}
T2 CF17E
这个题原题的数据不随机,考试的时候保证随机了,可以证明回文串个数期望非常少,于是就爆力直接通过。
正解做法是,考虑先用Manacher求出以每个位置作为中心的最长回文子串,把相交化成总的减去不交的,不交的方案数可以用前后缀和快速计算,复杂度 \(O(n)\)。
正解代码待补。
T3 CF1439C
由于那个单调性的原因,\(1\) 操作只需要二分加区间赋值,\(2\) 操作往后面取的段数最多也是 \(O(\log v)\) 的,所以可以直接使用线段树二分解决问题,复杂度 \(O(n\log n\log v)\)。
我写的是二分套线段树,多一个 \(\log\),亓神数据能过去但是cf过不去。正解代码待补。
T4 ARC058E
注意到 \(x+y+z\) 很小,考虑状压。另外,注意到三连击区间的右端点有一个共同特性,就是后缀和中都有 \(z,y+z,x+y+z\) 这三个值。
考虑一个dp:设 \(dp(i,S)\) 表示长度为 \(i\) 的序列,后缀和集合为 \(S\) 的方案数,状态数是 \(O(2^{x+y+z}n)\),使用刷表总复杂度 \(O(2^{x+y+z}nv)\)。
也可以在ACAM上跑dp,复杂度似乎是 \(O(2^{x+y+z}n^2)\),也可以通过。
正解代码待补。
Day 2-ljc
T1 CF1479B2
考虑一个贪心,假设两个末尾元素为 \(x,y\),进来一个 \(z\),如果 \(z\) 和 \(x,y\) 其中一个相等就放到那个序列里,否则放到 \(x,y\) 中离后面一个元素更近的序列里。看起来很对,然后就过了。
我的做法是能放则放,具体来说是维护一个栈表示当前后面待放的一些数,如果进来一个在栈中已经出现过的数,那么就把这个数放在最后,剩下的数放在另一个子序列里,这样看起来比较优,和上边的做法本质上应该是一样的。
点击查看代码
#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int N=1e5+13;
bool vis[N];
stack<int> s;
int main(){
freopen("unnamedone.in","r",stdin);
freopen("unnamedone.out","w",stdout);
int n,x,pre=0,cnt=0;
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&x);
if(x==pre){++cnt;continue;}
pre=x;
if(!vis[x]) s.push(x),vis[x]=1;
else{
++cnt;int tmp=s.top();
while(!s.empty()) vis[s.top()]=0,s.pop();
s.push(tmp),vis[tmp]=1;
s.push(x),vis[x]=1;
}
}
printf("%d\n",n-cnt);
return 0;
}
T2 P3505
考虑把和 \(1\) 相连的放在一起、和 \(2\) 相连的放在一起,其他点放在一起,这三个子图全部连成完全图,这样看起来很优。
考虑那个其他点的集合,由于 \(5\) 条边的限制,所以这个集合的每一个点都只能向上边两种的其中一种连边。如果已经有边连了,那没办法,否则就选择 \(siz\) 更大的那一边连边即可。可以证明没有方案能比这更大。
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=40000+13,M=1e6+13;
struct Edge{int v,nxt;}e[M<<1];
int n,m,h[N],tot;
int vis[N];
inline void add_edge(int u,int v){e[++tot]=(Edge){v,h[u]};h[u]=tot;}
inline int cnt_edge(int x){return x*(x-1)/2;}
int main(){
freopen("unnamedtwo.in","r",stdin);
freopen("unnamedtwo.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;++i) scanf("%d%d",&u,&v),add_edge(u,v),add_edge(v,u);
int cnt1=0,cnt2=0;
for(int i=h[1];i;i=e[i].nxt) vis[e[i].v]=1,++cnt1;
for(int i=h[2];i;i=e[i].nxt) vis[e[i].v]=2,++cnt2;
int ans=cnt_edge(cnt1+1)+cnt_edge(cnt2+1),cnt=0;
for(int i=3;i<=n;++i){
if(vis[i]) continue;
++cnt;int flag=0;
for(int j=h[i];j;j=e[j].nxt){
int v=e[j].v;
if(vis[v]) flag=vis[v];
}
if(!flag) ans+=max(cnt1,cnt2);
else if(flag==1) ans+=cnt1;
else ans+=cnt2;
}
ans+=cnt_edge(cnt);
printf("%d\n",ans-m);
return 0;
}
T3 CF883B
先正反两遍求出每个点权值的范围 \([l_u,r_u]\),考虑从小到大对每个 \(i\) 来确定点权为 \(i\) 的点。
当处理到 \(i\) 时,\([1,i-1]\) 已经处理完了,我们只需要处理还没被放点权的、\(l_u\leq i\) 的点。如果这些点中存在 \(r_u=i\) 的点,那么这些点的点权都只能是 \(i\),因为后面他们就不能放了;如果没有 \(r_u=i\) 的点,那么根据上面这个过程,找一个 \(r_u>i\) 的最小 \(r_u\) 的点 \(u\) 来放 \(i\) 一定不会更劣。如果处理到某个 \(i\) 时没有 \(l_u\leq i\) 且 \(r_u\geq i\) 的点,那么就无解。
正解代码待补。
T4 CF1416E
本次集训最难题(李神好毒瘤)。
首先把问题转化成最大化相邻两个数相等的对数。考虑一个dp:令 \(c_i=a_{2i-1},d_i=a_{2i}\),设 \(f(i,j)\) 表示考虑了 \(b\) 的前 \(i\) 个数,此时 \(d_i=j\) 的最大相邻相等对数。则有:
由于上面那个式子后面加的最多为 \(2\),说明对于同一个 \(i\) 来说,\(f_{i,j}\) 的最大值和最小值的差最多为 \(2\)。而且,比最小值差 \(2\) 的只有可能在 \(j=b_i\) 处取到。考虑维护 \(f\) 的最小值和比最小值大 \(1\) 的集合,以及比最小值大 \(2\) 的位置是否存在。
后面就是一大堆分类讨论转移,用 set
维护线段可以做到 \(O(n\log n)\)。正解代码待补。
Day 3-zsy
我的题,直接放题解了。
T1 P2668
注意到牌数其实不小,所以直接爆搜显然不是一个好的选择。考虑贪心地爆搜+最优化剪枝,贪心策略就是先顺子再从四个三个两个的出,最后是单牌,随机数据能过。
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<queue>
using namespace std;
const int INF=0x7fffffff;
int a[20],n,ans;
bool check(){
for(int i=1;i<=14;++i)
if(a[i]) return 0;
return 1;
}
void dfs(int d){
if(d>=ans) return;
for(int i=1;i<=6;++i){
switch(i){
case 1:{
for(int l=3;l<=10;++l){
if(!a[l]) continue;
int r=l+1;
while(r<=13&&a[r]) ++r;--r;
if(r==13&&a[1]) r++;
if(r-l+1<5) continue;
for(int j=l+4;j<=r;++j){
for(int k=l;k<=j;++k)
if(k<=13) a[k]--;
else a[1]--;
dfs(d+1);
for(int k=l;k<=j;++k)
if(k<=13) a[k]++;
else a[1]++;
}
}
}
case 2:{
for(int l=3;l<=12;++l){
if(a[l]<2) continue;
int r=l+1;
while(r<=13&&a[r]>=2) ++r;--r;
if(r==13&&a[1]>=2) ++r;
if(r-l+1<3) continue;
for(int j=l+2;j<=r;++j){
for(int k=l;k<=j;++k)
if(k<=13) a[k]-=2;
else a[1]-=2;
dfs(d+1);
for(int k=l;k<=j;++k)
if(k<=13) a[k]+=2;
else a[1]+=2;
}
}
break;
}
case 3:{
for(int l=3;l<=13;++l){
if(a[l]<3) continue;
int r=l+1;
while(r<=13&&a[r]>=3) ++r;--r;
if(r-l+1<2) continue;
for(int j=l+1;j<=r;++j){
for(int k=l;k<=j;++k)
if(k<=13) a[k]-=3;
else a[1]-=3;
dfs(d+1);
for(int k=l;k<=j;++k)
if(k<=13) a[k]+=3;
else a[1]+=3;
}
}
break;
}
case 4:{
for(int i=1;i<=13;++i){
if(a[i]<4) continue;
a[i]=0;
for(int j=1;j<=14;++j){
if(!a[j]) continue;
a[j]--;
for(int k=1;k<=14;++k){
if(a[k]){
a[k]--;
dfs(d+1);
a[k]++;
}
}
a[j]++;
}
for(int j=1;j<=13;++j){
if(a[j]<2) continue;
a[j]-=2;
for(int k=1;k<=13;++k){
if(a[k]>=2){
a[k]-=2;
dfs(d+1);
a[k]+=2;
}
}
a[j]+=2;
}
a[i]=4;
}
break;
}
case 5:{
for(int i=1;i<=13;++i){
if(a[i]>=3){
a[i]-=3;
for(int j=1;j<=13;++j){
if(i==j) continue;
if(a[j]>=2){
a[j]-=2;
dfs(d+1);
a[j]+=2;
}
}
a[i]+=3;
}
}
break;
}
case 6:{
for(int i=1;i<=13;++i){
if(a[i]>=3){
a[i]-=3;
for(int j=1;j<=14;++j){
if(i==j) continue;
if(a[j]>=1){
a[j]--;
dfs(d+1);
a[j]++;
}
}
a[i]+=3;
}
}
break;
}
}
}
for(int i=1;i<=14;++i)
if(a[i]) ++d;
ans=min(ans,d);
}
int main(){
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
int T;
scanf("%d%d",&T,&n);
while(T--){
ans=INF;
memset(a,0,sizeof(a));
for(int i=1,x,useless;i<=n;++i){
char s[3];
scanf("%s",s);
if(strlen(s)==1) x=s[0]-'0';
else x=(s[0]-'0')*10+s[1]-'0';
scanf("%d",&useless);
if(!x) a[14]++;
else a[x]++;
}
dfs(0);
printf("%d\n",ans);
}
return 0;
}
T2 CF1547G
首先要注意这个题的自环。
先把整个图缩强连通分量,在缩点后的DAG上跑拓扑排序,判一下强连通分量 \(siz>1\) 的(它和后面肯定都是 \(-1\)),判一下自环。剩下的 \(1\) 和 \(2\) 就简单dp就行了。
缩点我用的 kosaraju
算法。
点击查看代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#define pb push_back
#define mp make_pair
using namespace std;
typedef pair<int,int> pii;
const int N=4e5+13;
struct Edge{int v,nxt;}e[N];
int n,m,uu[N],vv[N],s[N],top,c[N],cnt_scc,h[N],tot,ind[N],ans[N],answer[N];
vector<int> e1[N],e2[N],e3[N],scc[N];
bool zihuan[N],have[N],vis[N],novis[N];
inline void clear(){
tot=0;
for(int i=1;i<=n;++i) zihuan[i]=have[i]=h[i]=ind[i]=ans[i]=0;
for(int i=1;i<=n;++i) e1[i].clear(),e2[i].clear(),e3[i].clear();
}
void dfs1(int u){
vis[u]=1;
for(auto v:e1[u])
if(!vis[v]) dfs1(v);
s[++top]=u;
}
void dfs2(int u,int num){
c[u]=num,scc[num].pb(u);
for(auto v:e2[u])
if(!c[v]) dfs2(v,num);
}
inline void kosaraju(){
for(int i=1;i<=n;++i) vis[i]=c[i]=0,scc[i].clear();
top=cnt_scc=0;
for(int i=1;i<=n;++i)
if(!vis[i]) dfs1(i);
for(int i=n;i;--i){
if(c[s[i]]) continue;
dfs2(s[i],++cnt_scc);
}
}
inline void bfs(int st){
for(int i=1;i<=cnt_scc;++i) vis[i]=0;
queue<int> q;while(!q.empty()) q.pop();
q.push(st);vis[st]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(auto v:e3[u])
if(!vis[v]) vis[v]=1,q.push(v);
}
for(int i=1;i<=cnt_scc;++i) novis[i]=!vis[i];
}
inline void add_edge(int u,int v){e[++tot]=(Edge){v,h[u]};h[u]=tot;ind[v]++;}
inline void topsort(){
queue<pii> q;while(!q.empty()) q.pop();
if(have[c[1]]) ans[c[1]]=-1;
else ans[c[1]]=1;
q.push(mp(c[1],ans[c[1]]));
while(!q.empty()){
int u=q.front().first,t=q.front().second;q.pop();
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].v;ind[v]--;
if(ans[v]!=-1){
if(t!=1) ans[v]=t;
else if(ans[v]!=-1&&ans[v]!=2) ++ans[v];
}
if(!ind[v]){
if(have[v]) ans[v]=-1;
q.push(mp(v,ans[v]));
}
}
}
}
inline void solve(){
scanf("%d%d",&n,&m);
clear();
for(int i=1;i<=m;++i){
scanf("%d%d",&uu[i],&vv[i]);
int u=uu[i],v=vv[i];
if(u==v) zihuan[u]=1;
e1[u].pb(v),e2[v].pb(u);
}
kosaraju();
for(int i=1;i<=n;++i)
if(zihuan[i]) have[c[i]]=1;
for(int i=1;i<=cnt_scc;++i)
if(scc[i].size()>1) have[i]=1;
for(int i=1;i<=m;++i)
if(c[uu[i]]!=c[vv[i]]) e3[c[uu[i]]].pb(c[vv[i]]);
bfs(c[1]);
for(int i=1;i<=m;++i)
if(c[uu[i]]!=c[vv[i]]&&!novis[c[uu[i]]]&&!novis[c[vv[i]]]) add_edge(c[uu[i]],c[vv[i]]);
topsort();
for(int i=1;i<=cnt_scc;++i)
for(auto u:scc[i]) answer[u]=ans[i];
for(int i=1;i<=n;++i) printf("%d ",answer[i]);puts("");
}
int main(){
freopen("b.in","r",stdin);
freopen("b.out","w",stdout);
int T;scanf("%d",&T);
while(T--) solve();
return 0;
}
T3 P5024
算法一:考虑动态dp求最大权独立集,把单点修改转化成把点权改为正/负无穷就能转化为板子 P4719 了。
算法二:
考虑一个朴素的dp做最大权独立集,每次询问重新做一遍,复杂度 \(O(nm)\)。期望得分 \(44\)。
对于树高 \(\leq 100\) 的情况,考虑记录一个“整棵树除去 \(i\) 子树的最小费用”,每次只需要对两个节点之间的链爆力跑dp即可。期望得分 \(52\)。
用倍增优化上述过程,一条链上的转移可以倍增进行,预处理出每个点到 \(2^j\) 级祖先,链头链尾选或不选时链上的dp值,每次询问的时候爆力往上跳即可。复杂度 \(O((n+m)\log n)\),期望得分 \(100\)。正解代码待补。
T4 P3622
注意到 \(5\) 很小,可以直接状压,每一位用 \(0/1\) 表示是否移走。
定义 \(num_{i,s}\) 表示开头为 \(i\) 的五个景观状态为 \(s\) 时,高兴的人个数。
设 \(dp_{i,s}\) 表示前 \(i\) 个位置为开头且第 \(i\) 个位置开头后面五个状态为 \(s\) 的时候,最多高兴的人个数,则有
环的话,最后处理注意一下细节即可。
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=5e4+4,M=44;
int n,m,num[N][M],f[2][M],ans;
int main(){
freopen("d.in","r",stdin);
freopen("d.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1,a,b,E,p1,p2,t;i<=m;++i){
scanf("%d%d%d",&E,&p1,&p2);
a=0,b=0;
while(p1--){
scanf("%d",&t);
t=(t-E+n)%n;
a|=(1<<t);
}
while(p2--){
scanf("%d",&t);
t=(t-E+n)%n;
b|=(1<<t);
}
for(int j=0;j<32;++j)
if((j&b)||((~j)&a)) num[E][j]++;
}
for(int i=0;i<32;++i){
memset(f,0xcf,sizeof(f));
f[0][i]=0;
bool v=1;
for(int j=1;j<=n;++j,v^=1){
for(int s=0;s<32;++s){
f[v][s]=max(f[v^1][(s&15)<<1],f[v^1][(s&15)<<1|1])+num[j][s];
}
}
ans=max(ans,f[n&1][i]);
}
printf("%d\n",ans);
return 0;
}
Day 4-zrz
T1 P4838
设 \(f_{i,j}\) 表示处理了前 \(i\) 位,最后有连续 \(j\) 个 \(0\) 的方案数,发现这个东西可以矩阵优化,直接矩阵快速幂即可,复杂度 \(O(k^3\log n)\)。
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=20+13,mod=1e8+7;
ll n;int m;
struct Matrix{
int d[N][N];
Matrix(){memset(d,0,sizeof d);}
Matrix operator *(Matrix A){
Matrix Ans;
for(int i=1;i<=m;++i)
for(int j=1;j<=m;++j)
for(int k=1;k<=m;++k)
Ans.d[i][j]=(Ans.d[i][j]+(ll)d[i][k]*A.d[k][j]%mod)%mod;
return Ans;
}
};
Matrix qpow(Matrix A,ll k){Matrix S=A;--k;for(;k;k>>=1,A=A*A)if(k&1)S=S*A;return S;}
inline void solve(){
scanf("%lld%d",&n,&m);
Matrix A;
for(int i=1;i<=m;++i) A.d[1][i]=1;
for(int i=2;i<=m;++i) A.d[i][i-1]=1;
Matrix Ans=qpow(A,n);
int res=0;
for(int i=1;i<=m;++i) res=(res+Ans.d[i][1])%mod;
printf("%d\n",res);
}
int main(){
freopen("these.in","r",stdin);
freopen("these.out","w",stdout);
int T;scanf("%d",&T);
while(T--) solve();
return 0;
}
T2 P4318
首先,这个“第 \(k\) 个”看起来很难求,考虑二分答案,计算 \(mid\) 以内的个数即可。个数的计算需要一个容斥,大概的式子是
预处理出所有答案范围内的这样的只有平方质因子的数,统计答案即可。注意到 \(k\) 范围内这样的数只有 \(O(\sqrt k)\) 个,所以总复杂度 \(O(\sqrt k\log k)\)。
这题也可以把刚才那个式子化成 \(\mu^2\) 前缀和,但是更麻烦一点,复杂度不变常数稍大。
点击查看代码
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
bool b[100005];
int prm[100005],pcnt,cnt;
ll a[100005],lim,tot;
int k;
inline void init(){
for(int i=2;i<=100000;++i){
if(!b[i]) prm[++pcnt]=i;
for(int j=1;j<=pcnt&&i*prm[j]<=100000;++j){
b[i*prm[j]]=1;
if(i%prm[j]==0) break;
}
}
for(int i=1;i<=pcnt;++i) a[i]=1ll*prm[i]*prm[i];
}
void dfs(int k,int flag,ll sum){
tot+=flag*(lim/sum);
if(k==cnt+1) return;
for(int i=k;i<=cnt;++i){
if(sum<=lim/a[i]) dfs(i+1,flag*-1,sum*a[i]);
else break;
}
}
inline bool check(ll x){
cnt=pcnt;
while(a[cnt]>x) --cnt;
tot=0,lim=x;
for(int i=1;i<=cnt;++i) dfs(i+1,1,a[i]);
return x-tot>=k;
}
inline void solve(){
scanf("%d",&k);
ll l=k,r=10ll*k,mid;
while(l<r){
mid=(l+r)>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
printf("%lld\n",l);
}
int main(){
freopen("all.in","r",stdin);
freopen("all.out","w",stdout);
init();
int T;scanf("%d",&T);
while(T--) solve();
return 0;
}
T3 CF45G
考虑哥德巴赫猜想:任何一个大于 \(4\) 的偶数都可以拆成两个奇质数的和,这个猜想在偶数很小的时候都是成立的。
分类讨论之后,对于偶数的情况,可以先找出两个质数,再用 \(1\sim n\) 的这些数拼出这两个质数即可。可以证明一定有方案可以拼出来。
点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
inline bool isprime(int x){
for(int i=2;i*i<=x;++i)
if(x%i==0) return 0;
return 1;
}
const int N=6000+13;
bool vis[N];
int main(){
freopen("tick.in","r",stdin);
freopen("tick.out","w",stdout);
int n;scanf("%d",&n);
int tmp=n*(n+1)/2;
if(tmp&1){
if(isprime(tmp)){
for(int i=1;i<=n;++i) printf("1 ");
return puts(""),0;
}
if(isprime(tmp-2)){
printf("1 2 ");for(int i=3;i<=n;++i) printf("1 ");
return puts(""),0;
}
tmp-=3;
int k=0;
for(int i=2;i<=tmp/2;++i)
if(isprime(i)&&isprime(tmp-i)){k=i;break;}
int p=n;
while(k>p) vis[p]=1,k-=p,--p;
if(k) vis[k]=1;
if(vis[3]==1) vis[1]=vis[2]=1;
for(int i=1;i<=n;++i){
if(i==3) printf("3 ");
else printf("%d ",1+vis[i]);
}
}
else{
int k=0;
for(int i=2;i<=tmp/2;++i)
if(isprime(i)&&isprime(tmp-i)){k=i;break;}
int p=n;
while(k>p) vis[p]=1,k-=p,--p;
if(k) vis[k]=1;
for(int i=1;i<=n;++i) printf("%d ",1+vis[i]);
puts("");
}
return 0;
}
这个题还有一个点在于,李神的代码只枚举了极少的情况,好像是说两个质数中一定有一个可以只被两个 \(1\sim n\) 的数相加得到。不知道这是因为数据水,还是可以证明这是对的。
T4 P3653
注意到 \(r-l\leq 10^5\),考虑用 \(10^6\) 以内的质数对这些数做类似埃氏筛的操作,复杂度应该是 \(O(10^5\cdot \log 10^6)\) 左右。
把那些 \(\mu=0\) 的数都去掉,剩下的数,对于大质数来说只有三种可能:\(P^2,P,PQ\)。\(P^2\) 是好判的,直接判完全平方和质数即可,\(P\) 可以使用 Miller-Rabin
算法来快速判质数(判个 \(10\sim 30\) 次就行了),由于这 \(10^5\) 个数是连续的,所以不会有很多进入 Miller-Rabin
测试,可以通过此题。
正解代码待补。