CSP 前垂死挣扎 (2022.9~2022.10)
马上要 CSP 了, 急了急了.
upd: 关于 CSP: 它死了.
对 luogu 题目难度的评价: 绿>蓝.
1. P3147 262144 P
奇怪的 dp 题.
考虑定义状态 \(f(i,k)\) 表示, 从位置 \(i\) 开始能合并出数 \(k\) 的区间为 \([i,f(i,k))\).
容易发现 \(f(i,k)=f(f(i,k-1),k-1)\), 转移复杂度 \(O(n\log n)\).
for(int i=1;i<=n;i++)
{
int qwq=read();
dp[i][qwq]=i+1;
}
for(int k=2;k<=58;k++)//40+log2(262144)=58
for(int i=1;i<=n;i++)
{
if(!dp[i][k])dp[i][k]=dp[dp[i][k-1]][k-1];
if(dp[i][k])ans=max(ans,k);
}
2. P1712 区间
首先离散化并按照原来的区间长度排个序, 然后尺取即可.
判断是否合法就是区间加区间 \(\text{max}\).
for(int l=1;l<=n;l++)
{
while(r<n&&tree[1].val<m)
{
r++;
modify(1,a[r].l,a[r].r,1);
}
if(tree[1].val<m)break;//全局查询直接取用根节点的值就行
ans=min(ans,a[r].len-a[l].len);
modify(1,a[l].l,a[l].r,-1);
}
3. P6033 合并果子 加强版
我们需要 \(O(n)\).
首先桶排, 然后还是做取出最小值, 合并, 插入的操作.
然后你注意到每次取出的最小值单调递增 (显然), 于是实际上不需要优先队列, 直接将合并的结果扔到另一个队列里就行了. 每次就是从两个单调递增的队列中选两个最小值合并.
4. P1333 选择客栈 & P6032 选择客栈 加强版
对每种颜色写一个前缀和进行查询就是 \(O(nk)\), 即使你没有任何的枚举技巧, 二分 +ST 表也能通过原题.
要做加强版我们需要一点小技巧.
具体地, 我们从左往右枚举右端点, 然后考虑对于固定的右端点, 移动左端点, 计算新增的贡献.
我们发现左端点只需从上一个花费 \(\leq p\) 的位置枚举到上上一个花费 \(\leq p\) 的位置就行了, 因为我们是从左往右枚举右端点, 上上一个花费 \(\leq p\) 的位置及其之前的位置的贡献肯定已经被计算过了, 我们只需把这个已经算过的贡献加上就行了.
另外注意左右端点不能重合.
这样每个位置只会算一次, 时间复杂度 \(O(n)\).
有点抽象, 放一下 code.
for(int i=1;i<=n;i++)
{
a[i].color=read();a[i].cost=read();
if(a[i].cost<=p)
{
cnt[a[i].color]++;
int j=i-1;
while(a[j].cost>p)cnt[a[j--].color]++;
}
ans+=cnt[a[i].color];
if(a[i].cost<=p)ans--;//排除左右端点重合的情况
}
5. P5278 算术天才与等差数列
大数据结构, 我写法丑陋写了 \(5.31\text{KB}\) (悲
一个区间重排后可以形成等差数列等价于:
- \(\max-\min=(r-l)k\);
- \(\gcd_{i=l}^{r-1} (a_{i+1}-a_{i})=k\);
- 无重复元素.
前面两条线段树随手维护.
最后一条就比较毒瘤了, 对于每个位置 \(p\), 我们考虑求出使得 \(a_x=a_p\) 的最大的 \(x(x<p)\). 然后就变成了区间 \(\max\).
求 \(x\) 考虑 map 套 set, 将数列中的值映射成位置的集合, 然后查前驱后继就行了.
具体实现一车细节, 包括区间长度为 \(1\), \(k=0\), set 查前驱后继的边界.
并且因为个人写法的原因还手写了双向链表.
附上个人写的 modify 和 check (写的好丑啊)
void modify(int pos,int val)
{
if(a[pos]==val)return;
modify(1,pos,val);
if(pos!=n)modify2(1,pos,abs(a[pos+1]-val));
if(pos!=1)modify2(1,pos-1,abs(a[pos-1]-val));
set<int>::iterator it=mp[a[pos]].find(pos);
if(pre[pos])suf[pre[pos]]=suf[pos];
if(suf[pos])pre[suf[pos]]=pre[pos];
if(suf[pos]!=0)modifypos(1,suf[pos],pre[pos]);
mp[a[pos]].erase(it);
a[pos]=val;
if(mp[val].size()==0)
{
mp[val].insert(pos);
pre[pos]=suf[pos]=0;
modifypos(1,pos,0);
return;
}
it=mp[val].lower_bound(pos);
if(it==mp[val].end())
{
it--;
pre[pos]=*it;
suf[*it]=pos;
modifypos(1,pos,*it);
mp[val].insert(pos);
return;
}
int now=*it;
if(it==mp[val].begin())
{
pre[now]=pos;
suf[pos]=now;
modifypos(1,now,pos);
mp[val].insert(pos);
return;
}
it--;
pre[now]=pos;
suf[pos]=now;
modifypos(1,now,pos);
pre[pos]=*it;
suf[*it]=pos;
modifypos(1,pos,*it);
mp[val].insert(pos);
return;
}
bool check(int x,int y,int k)
{
if(x==y){ycnt++;return 1;}
int maxx=query_max(1,x,y),minn=query_min(1,x,y);
if(maxx-minn!=(y-x)*k)return 0;
if(k==0){ycnt++;return 1;}
int gcdd=query_gcd(1,x,y-1);
if(gcdd!=k)return 0;
int maxp=query_maxp(1,x,y);
if(maxp<x){ycnt++;return 1;}
return 0;
}
6. P2127 序列排序
发现置换构成了一些环, 并查集维护环, 分环内最小值环外最小值两种情况讨论即可.
ans+=min(sum[now]+(siz[now]-2)*minv[now],sum[now]+minv[now]+(siz[now]+1)*minn);
7. CF676C Vasya and String
尺取秒了.
while(r<=n)
{
if(s[r-1]=='a')nowa++;
else nowb++;
if(nowa<=k||nowb<=k){r++;ans++;}
else
{
if(s[l-1]=='a')nowa--;
else nowb--;
l++;r++;
}
}
8. CF911D Inversion Counting
结论题. 发现奇偶变化只和翻转区间的长度有关.
if(len%4==0||len%4==1)//len(len-1)/2 mod 2=0
{
if(cnt%2)printf("odd\n");
else printf("even\n");
}
else
{
if(cnt%2)printf("even\n");
else printf("odd\n");
cnt++;
}
9. AGC008B Contiguous Repainting
显然通过操作我们会让一段长为 \(K\) 的区间颜色相同, 而其他位置的颜色可以做到任意确定.
于是这就变成了前缀和好题(
ans=max(ans,max(0ll,sum[i+k-1]-sum[i-1])+psum[i-1]+ssum[i+k]);
10. AGC006D Median Pyramid Hard
二分塔顶, 注意到连续两个同时大于/小于等于当前数的就会一路推上去.
于是塔顶与当前二分的数的相对大小就是离中间最近的那一对与当前二分的数的相对大小.
记得特判没有两个相邻数的相对大小相同的情况.
bool check(int now)
{
for(int i=1;i<=n-1;i++)
{
int l=n-i,r=n+i-1;
if(a[l]<=now&&a[l+1]<=now)return 1;
if(a[l]>now&&a[l+1]>now)return 0;
if(a[r]<=now&&a[r+1]<=now)return 1;
if(a[r]>now&&a[r+1]>now)return 0;
}
return a[1]<=now?1:0;
}
11. AGC020C Median Sum
注意到能组成的和有对称性. (若 \(x\) 能组成, \(sum-x\) 也能组成)
bitset, 每加一个数就 \(f|=f<<x\), 就把所有可能凑出来的数都涵盖了.
从中间开始枚举, 做完了.
st[0]=1;//记得初始化
for(int i=1;i<=n;i++)
{
int x=read();
sum+=x;
st|=st<<x;
}
for(int i=(sum+1)/2;i<=sum;i++)//实际上不完全有对称性, 0 被叉掉了. 所以要注意枚举的左端点.
if(st[i])
{
printf("%d",i);
return 0;
}
12. ABC107D Median of Medians
和 AGC006D 相似, 我们二分中位数并对数列中的数赋值 1/-1.
然后你发现问题神奇地变成了求区间和非负的区间数量.
做一遍前缀和, 二维偏序即可.
bool check(int now)
{
for(int i=1;i<=n;i++)
if(a[i]>=now)qwq[i]=1;
else qwq[i]=-1;
memset(sum,0,sizeof(sum));
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+qwq[i];
memset(tree,0,sizeof(tree));
int cnt=0;
for(int i=1;i<=n;i++)if(sum[i]>=0)cnt++;//左端点是 1 的要单独算
for(int i=1;i<=n;i++)
{
int cur=sum[i]+n+1;//树状数组的位置不能是负的
cnt+=query(cur);
add(cur,1);
}
if(cnt*2>=total)return 1;
return 0;
}
13. ARC075E Meaningful Mean
随便化一化式子发现又变成了二维偏序.
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i];
for(int i=1;i<=n;i++)val[i]=qwq[i]=sum[i]-k*i;
for(int i=1;i<=n;i++)if(val[i]>=0)ans++;//左端点为 1 还是要单独算
sort(qwq+1,qwq+n+1);
int pos=unique(qwq+1,qwq+n+1)-qwq-1;
for(int i=1;i<=n;i++)val[i]=lower_bound(qwq+1,qwq+n+1,val[i])-qwq;//离 散 化
for(int i=1;i<=n;i++)
{
ans+=query(val[i]);
add(val[i],1);
}
14. CF475D CGCDSSQ
不同 \(\gcd\) 的个数是 \(n\log n\) 级别的.
具体操作的时候固定左端点, 右端点每次二分+ST表就能求出 \(\gcd\) 变化的位置.
for(int l=1;l<=n;l++)
{
int r=l,now;
while(r<=n)
{
now=query(l,r);
int ql=r,qr=n,ans=n+1;
while(ql<=qr)
{
int mid=(ql+qr)>>1;
if(query(l,mid)!=now)
{
ans=mid;
qr=mid-1;
}
else ql=mid+1;
}
mp[now]+=ans-r;
r=ans;
}
}
15. P5663 加工零件
容易发现只需求出各个点的长度为奇数/偶数的最短路.
个人认为最好想的是分层图, 一层奇一层偶, 两层之间连边跑最短路即可.
16. P5687 网格图
Kruskal 思想, 要避免成环, 每行/列当前能用的边数能直接用已经选的列/行数量算出来.
sort(a+1,a+n+1);sort(b+1,b+m+1);
ans+=a[1]*(m-1ll)+b[1]*(n-1ll);
pa=pb=2;
while(1)
{
if(pa>n||pb>m)break;
if(a[pa]<=b[pb])ans+=a[pa++]*(m-pb+1ll);
else ans+=b[pb++]*(n-pa+1ll);
}
17. P5658 括号树
首先写一个可撤销栈求出来右括号位置 \(u\) 对应的左括号的位置 \(last(u)\) (之前没写过这玩意, 差点被送走了)
记 \(f(u)\) 为以 \(u\) 结尾的合法串数量, 显然 \(f(u)=f(fa(last(u)))+1\). 最后从上往下做一个求和就行了.
void dfs(int u)
{
int moved=0,flag=0;
if(s[u-1]=='(')
{
moved=1;
st1.push(u);
}
else
{
flag=1;
if(!st1.empty())
{
moved=1;
last[u]=st1.top();
st2.push(st1.top());
st1.pop();
}
}
if(last[u])ans[u]=ans[fa[last[u]]]+1;
for(int i=h[u];i;i=e[i].nxt)
{
int p=e[i].to;
if(p==fa[u])continue;
dfs(p);
}
if(!moved)return;
if(flag)
{
if(!st2.empty())
{
st1.push(st2.top());
st2.pop();
}
}
else st1.pop();
}
18. P7915 回文
诈 骗 题.
考虑将回文的 \(b\) 还原成 \(a\) 你就会明白怎么做.
19. P1155 双栈排序
一个序列可以被单栈排序当且仅当没有 231 型子序列.
于是每出现一组 231, 2 和 3 就不能放到一个栈中.
连边黑白染色即能判断是否有解.
得出方案模拟为了保证优先级又是一车细节.
//最后模拟的代码
inline void mustpopst1()
{
while(!st1.empty()&&a[st1.top()]==now+1)
{
now++;
st1.pop();
printf("b ");
}
}
inline void mustpopst2()
{
while(!st2.empty()&&a[st2.top()]==now+1)
{
now++;
st2.pop();
printf("d ");
}
}
//------------------------------------------------
for(int i=1;i<=n;i++)
if(val[i]==0)
{
while(!st1.empty()&&a[st1.top()]<a[i])
{
mustpopst2();
now++;
st1.pop();
printf("b ");
}
st1.push(i);
printf("a ");
}
else
{
mustpopst1();
while(!st2.empty()&&a[st2.top()]<a[i])
{
now++;
st2.pop();
printf("d ");
}
mustpopst1();
st2.push(i);
printf("c ");
}
while(!st1.empty()||!st2.empty())
{
mustpopst1();
mustpopst2();
}
20. P1613 跑路
Floyd+倍增, 核心就一行:
f[i][j][k]|=f[i][x][k-1]&f[x][j][k-1];
21. CF875F Royal Questions
左部图点的度数都为 \(2\) 的二分图最大匹配.
考虑神奇建边, 每次将连接的两个右部图的点连起来.
然后你发现合法方案就是给边定向使每个点的入度小于等于 \(1\).
于是合法的边最多就是 \(n\) 条, 一棵基环外向树.
于是只需跑最大基环生成森林, 对 Kruskal 稍加改动即可.
for(int i=1;i<=m;i++)
{
int ru=find(e[i].u),rv=find(e[i].v);
if(ru==rv)
{
if(!cycle[ru])
{
cycle[ru]=1;
ans+=e[i].val;
}
}
if(ru!=rv&&cycle[ru]+cycle[rv]<=1)
{
fa[ru]=rv;
cycle[rv]+=cycle[ru];
ans+=e[i].val;
}
}
22. CF571B Minimization
首先发现这将 A 分成了若干组, 每组取到最大值当且仅当是顺序/逆序的.
于是取到最小值的方案只能是将 A 排序后, 从前往后划分成若干段.
注意到组只有两种可能长度, 我们用使用两种长度的个数设状态随便 dp 一下就行了.
for(int i=1;i<=x;i++)
dp[i][0]=dp[i-1][0]+a[(i-1)*s+s]-a[(i-1)*s+1];
for(int i=1;i<=y;i++)
dp[0][i]=dp[0][i-1]+a[(i-1)*(s+1)+s+1]-a[(i-1)*(s+1)+1];
for(int i=1;i<=x;i++)
for(int j=1;j<=y;j++)
dp[i][j]=min(dp[i-1][j]+a[(i-1)*s+j*(s+1)+s]-a[(i-1)*s+j*(s+1)+1],dp[i][j-1]+a[i*s+(j-1)*(s+1)+s+1]-a[i*s+(j-1)*(s+1)+1]);
23. P1972 HH的项链
典中典之区间数颜色, 但是我之前从来没写过.
考虑离线, 将询问按照右端点排序. 然后我们从前往后加贡献, 如果这个数之前出现过就把先前的贡献删掉.
于是这就变成了单点修改区间求和, 使用树状数组即可.
int last=1;
for(int i=1;i<=m;i++)
{
for(int j=last;j<=q[i].r;j++)
{
if(maxp[a[j]])add(maxp[a[j]],-1);//maxp 表示该位置的数最靠后的出现位置
add(j,1);maxp[a[j]]=j;
}
q[i].ans=querysum(q[i].r)-querysum(q[i].l-1);
last=q[i].r;
}
24. P4141 消失之物
我们发现每个物品就像是给 dp 数组滚了一个类似于前缀和的东西.
于是我们只需要每次把这个物品的贡献减掉就行了.
for(int i=1;i<=n;i++)
for(int j=m;j>=w[i];j--)
{
f[j]+=f[j-w[i]];
f[j]%=10;
}
for(int i=1;i<=n;i++)
{
memcpy(g,f,sizeof(f));
for(int j=w[i];j<=m;j++)
{
g[j]-=g[j-w[i]];
g[j]=(g[j]+10)%10;
}
for(int j=1;j<=m;j++)printf("%d",g[j]);
printf("\n");
}
25. P1858 多人背包
你发现每次求前 \(k\) 优解就是把两种方式 (选或不选) 所对应的前 \(k\) 优解构成的有序队列合并起来.
for(int i=1;i<=n;i++)
for(int j=V;j>=w[i];j--)
{
int cnt=0,l=1,r=1;
while(1)
{
if(cnt==k)break;
if(f[j][l]>f[j-w[i]][r]+v[i])now[++cnt]=f[j][l++];
else now[++cnt]=f[j-w[i]][r++]+v[i];
}
for(int qwq=1;qwq<=k;qwq++)f[j][qwq]=now[qwq];
}
26. P1131 时态同步
因为边权只能增加, 我们考虑将所有的深度都改成最大的那个.
修改的时候尽量在靠近根的地方改.
void dfs2(int u,int fa,int now)//now 表示到当前点已经改了多少
{
for(int i=h[u];i;i=e[i].nxt)
{
int p=e[i].to;
if(p==fa)continue;
ans+=maxv-maxd[p]-now;//maxd 是子树内最大深度
dfs2(p,u,maxv-maxd[p]);
}
}
27. CF111C Petya and Spiders
转换成四连通块覆盖棋盘, 状压 dp 即可.
for(int s1=0;s1<(1<<n);s1++)f[1][s1][0]=ppcnt(s1);
for(int i=2;i<=m;i++)
for(int s1=0;s1<(1<<n);s1++)
for(int s2=0;s2<(1<<n);s2++)
for(int s3=0;s3<(1<<n);s3++)
if(((s1|s2|s3|(s2<<1)|(s2>>1))&(1<<n)-1)==(1<<n)-1)
f[i][s1][s2]=min(f[i][s1][s2],f[i-1][s2][s3]+ppcnt(s1));
for(int s1=0;s1<(1<<n);s1++)
for(int s2=0;s2<(1<<n);s2++)
if(((s1|s2|(s1<<1)|(s1>>1))&(1<<n)-1)==(1<<n)-1)
ans=min(ans,f[m][s1][s2]);
28. P6192 【模板】最小斯坦纳树
设 \(f(i,S)\) 为包含点 \(i\), 且已经选取到的关键点的状态为 \(S\).
两种转移:
- \(f(i,S)=f(i,T)+f(i,S-T), T\sube S\)
- \(f(i,S)=f(j,S)+w(j,i)\)
对于前一种, 枚举子集即可.
对于后一种, 仿照 dijkstra 即可.
//dijkstra略
for(int i=1;i<=k;i++)
{
rt[i]=read();
f[rt[i]][1<<(i-1)]=0;
}
for(int s=1;s<(1<<k);s++)
{
for(int i=1;i<=n;i++)
{
for(int t=s&(s-1);t;t=s&(t-1))f[i][s]=min(f[i][s],f[i][t]+f[i][s^t]);
if(f[i][s]!=inf)q.push(make_pair(f[i][s],i));
}
dijkstra(s);
}
printf("%d\n",f[rt[1]][(1<<k)-1]);
29. P7913 廊桥分配
我们考虑每进入一辆飞机, 就让它停到能停的编号最小的廊桥.
一个思想上比较简单的写法是, 以廊桥编号为下标, 离开时间为值开线段树, 记录区间离开时间的最小值.
每次新加入一架飞机, 在线段树上二分出最小的下标即可.
int find(int x,int w)//线段树上二分
{
if(tree[x].l==tree[x].r)
{
if(tree[x].val<w)return tree[x].l;
return inf;
}
int lson=x<<1,rson=lson|1;
if(tree[lson].val>w&&tree[rson].val>w)return inf;
if(tree[lson].val<w)return find(lson,w);
return find(rson,w);
}
//main函数内
sort(p+1,p+m1+1,cmp);//一定记得按到达时间排序
build(1,1,maxn);
for(int i=1;i<=m1;i++)
{
int now=find(1,p[i].a);
if(now==inf)
{
cnt++;//cnt为现在所需的廊桥数
use1[cnt]++;
modify(1,cnt,p[i].b);
continue;
}
use1[now]++;
modify(1,now,p[i].b);
}
for(int i=1;i<=n;i++)use1[i]+=use1[i-1];
//另一部分同理
for(int i=0;i<=n;i++)ans=max(ans,use1[i]+use2[n-i]);
30. CF176E Archaeology
你发现就是要维护 \(\dfrac{1}{2}(\text{dis}(u_1,u_2)+\text{dis}(u_2,u_3)+\cdots+\text{dis}(u_{k-1},u_k)+\text{dis}(u_k,u_1))\).
直接上 set, 边界情况特判一下就行了.
只放插入的代码.
if(opt=='+')
{
int u;cin >> u;
st.insert(dfn[u]);
set<int>::iterator it=st.find(dfn[u]);
if(st.size()==1)continue;
if(st.size()==2)
{
set<int>::iterator r=st.end();r--;
ans+=2*d(pos[*(st.begin())],pos[*r]);
continue;
}
set<int>::iterator l,r;
if(it==st.begin())l=st.end();
else l=it;
l--;r=it;r++;
if(r==st.end())r=st.begin();
ans+=d(pos[*l],pos[*it]);
ans+=d(pos[*it],pos[*r]);
ans-=d(pos[*l],pos[*r]);
}