10.28
1 模拟赛 把双向扫描线写了
2 rabbit_mygo 模拟赛
3 rabbit_mygo 推荐题
4 自己找的题 做两~四道然后标签里的
t1
题意
现在有两个项链 \(s\) 和 \(t\),其中的每一颗珠子都可以用一个小写英文字母表示。现在,你需要帮敖丙从项链 \(s\) 中拿走一些珠子,使得项链 \(s\) 不存在一个连续的子串与 \(t\) 相同。
对于所有数据,保证 \(1 \leq |s| \leq 5 \times 10^4\),\(1 \leq |t| \leq 10^3\),且 \(s\)、\(t\) 均仅包含小写英文字母。
题解
考虑设 \(dp_{i,j}\) 为 \(S\) 串匹配到 \(i\),\(T\) 串匹配到 \(j\),可以保留的最多点数。那么用 AC自动机 或者 kmp 或者 哈希预处理每种字母选上的匹配变化即可,具体是选或者不选,时间复杂度 \(O(|S||T|)\)。
#include<bits/stdc++.h>
#define int long long
#define m128(a) memset(a,-0x3f,sizeof(a))
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=1e3+10,inf=0x3f3f3f3f;
int ch[N][26],f[N],tot=0,dp[2][N],ans;
void add(string s){
int pl=0,len=s.size()-1;
rep(i,1,len){
int num=s[i]-'a';
if(!ch[pl][num]){
ch[pl][num]=++tot;
}pl=ch[pl][num];
}
}
void getfail(){
queue<int> q;
rep(i,0,25){
if(ch[0][i]){
q.push(ch[0][i]);
}
}while(!q.empty()){
int u=q.front(); q.pop();
rep(i,0,25){
int v=ch[u][i];
if(!v){
ch[u][i]=ch[f[u]][i];
continue;
}f[v]=ch[f[u]][i];
q.push(v);
}
}
}
string s,t;
signed main(){
freopen("neck.in","r",stdin);
freopen("neck.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>s>>t;s=" "+s,t=" "+t;
add(t),getfail();
int len=s.size()-1,pl=0;
m128(dp[pl]);dp[pl][0]=0;
rep(i,0,len-1){
pl^=1;
m128(dp[pl]);
int to=s[i+1]-'a';
rep(j,0,tot-1){
dp[pl][j]=max(dp[pl][j],dp[pl^1][j]);
dp[pl][ch[j][to]]=max(dp[pl][ch[j][to]],dp[pl^1][j]+1);
}
}rep(i,0,tot-1) ans=max(ans,dp[pl][i]);
cout<<ans;
return 0;
}
t2
考虑到任意交叉一定可以交换成不交叉的情况,所以我们把图变成树,接下来就是一个经典问题,树上无边交最大匹配。做法如下:
- 设计 \(dp_{i,0/1}\) 表示到 \(i\) 还是否有点没匹配,贪心的从下往上做,能匹配就匹配,这样是对的,顺便把答案记下来。
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define int long long
#define pii pair<int,int>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=1e5+10;
vector<int> e[N],u,d;
int n,m,k,flg[N],vis[N],f[N],lft[N],dep[N];
vector<pii>ans;
void dfs(int u,int fa){
vis[u]=1;f[u]=fa;dep[u]=dep[fa]+1;
int lst=0;for(auto v:e[u]){
if(vis[v]) continue;
dfs(v,u);
if(lft[v]){
if(lst) ans.pb({lst,lft[v]}),lst=0;
else lst=lft[v];
}
}if(flg[u]){
if(lst) ans.pb({lst,u}),lst=0;
else lst=u;
}lft[u]=lst;
}
void gs(int x,int y){
u.clear(),d.clear();
if(dep[x]<dep[y]) swap(x,y);
while(dep[x]>dep[y]){
u.push_back(x);x=f[x];
}while(x!=y){
u.pb(x),d.pb(y);
x=f[x],y=f[y];
}cout<<u.size()+d.size()+1<<" ";
for(auto i:u) cout<<i<<" ";
cout<<x<<" ";
reverse(d.begin(),d.end());
for(auto i:d) cout<<i<<" ";
cout<<'\n';
}
signed main(){
freopen("travel.in","r",stdin);
freopen("travel.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>k;
rep(i,1,m){
int u,v;cin>>u>>v;
e[u].pb(v),e[v].pb(u);
}rep(i,1,k){
int x;cin>>x;
flg[x]=1;
}rep(i,1,n){
if(vis[i])continue;
dfs(i,0);
}cout<<ans.size()<<'\n';
for(auto i:ans){
gs(i.fi,i.se);
}return 0;
}
t3
这东西在线不太能做,考虑离线扫描。扫描右端点 \(r\),我们对每个位置 \(l\) 维护一个 \(p_l\) 表示最小的 \(p\) 使得 \([l,p]\) 是 \([l,r]\) 的合法子区间。
考虑如何维护 \(p_l\)。考虑新加入的右端点 \(r\),加入一个数 \(a_r\),上一次出现的位置为 \(lst_{a_r}=c\),那么对于 \(c\) 和以前的位置是没有影响的;而 \((c,r]\) 这段区间由于没有 \(a_r\) 这个数,那么直接覆盖为 \(r\) 即可。可以用线段树维护。
考虑查询,当前扫描到 \(r\),询问 \([l,r]\)。令 \(q_l\) 为最大的 \(q\) 使得 \([q,r]\) 合法,那么不难发现答案就是 \(\min\limits_{i\in[l,q_l]}\{p_i-i\}\)。这东西可以线段树上二分,但是多带了一半常数,估计过不去。
有一个很妙的并查集做法,学会了。
就是要对每个 \(l\) 维护 \(q_l\),当插入 \(r\) 前已经有一个 \(lst_{a_r}\) 了,那么直接将 \(q_{lst_{a_r}}\to lst_{a_r}+1\) 即可。因为 \(q_l\) 相当于 \([l,r]\) 所有出现过的数的 \(lst\) 取 \(\min\), \(lst_{a_r}\) 这个位置可以替换成 \(r\) 了,所以直接求 \(q_{lst_{a_r}+1}\) 即可。
我代码太粪了,过不了这边的数据,但是比赛那边能过。
我的做法就是发现这个 \(q_l\) 实际上就是反着跑的 \(q\) 数组,所以我做了两遍扫描线。
#include<bits/stdc++.h>
#define fi first
#define se second
#define ls (p<<1)
#define rs (p<<1|1)
#define pb push_back
#define lbt(x) (x&(-x))
#define pii pair<int,int>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=2e6+10;
int n,m,q,k,T,a[N],t[N<<2],laz[N<<2];
int lst[N],c[N],nxt[N],d[N],ans[N];
vector<pii> p1[N],p2[N];
void pushup(int p){
t[p]=min(t[ls],t[rs]);
}
void pushdown(int p,int l,int r){
if(laz[p]==0) return;
int k=laz[p];laz[p]=0;
int mid=l+r>>1;
t[ls]=k-mid;t[rs]=k-r;
laz[ls]=laz[rs]=k;
}
void build1(int p,int l,int r){
laz[p]=0;
if(l==r){
t[p]=n+1-l;return;
}int mid=l+r>>1;
build1(ls,l,mid);build1(rs,mid+1,r);
pushup(p);
}
void build2(int p,int l,int r){
laz[p]=0;
if(l==r){
t[p]=-l;return;
}int mid=l+r>>1;
build2(ls,l,mid);build2(rs,mid+1,r);
pushup(p);
}
void upd(int p,int l,int r,int x,int y,int k){
if(x<=l&&r<=y){
t[p]=k-r;laz[p]=k;return;
}int mid=l+r>>1;pushdown(p,l,r);
if(x<=mid) upd(ls,l,mid,x,y,k);
if(y>mid) upd(rs,mid+1,r,x,y,k);
pushup(p);
}
int query(int p,int l,int r,int x,int y){
if(x<=l&&r<=y){
return t[p];
}int mid=l+r>>1,res=INT_MAX;pushdown(p,l,r);
if(x<=mid) res=min(res,query(ls,l,mid,x,y));
if(y>mid) res=min(res,query(rs,mid+1,r,x,y));
return res;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
rep(i,1,n){
cin>>a[i];lst[i]=c[a[i]];
c[a[i]]=i;
}rep(i,1,n) c[i]=n+1;
per(i,n,1){
nxt[i]=c[a[i]];
c[a[i]]=i;
}cin>>q;rep(i,1,q){
int l,r;cin>>l>>r;
p1[l].pb({r,i});p2[r].pb({l,i});
}build1(1,1,n);
per(i,n,1){
upd(1,1,n,i,nxt[i]-1,i);
for(auto t:p1[i]){
int v=t.fi,id=t.se;
d[id]=query(1,1,n,v,v)+v;
}
}build2(1,1,n);
rep(i,1,n){
upd(1,1,n,lst[i]+1,i,i);
for(auto t:p2[i]){
int v=t.fi,id=t.se;
ans[id]=query(1,1,n,v,d[id])+1;
}
}rep(i,1,q) cout<<ans[i]<<'\n';
return 0;
}
t4
显然这题的答案满足可二分性。
问题变成判断是否存在一种方案,能使所有的叶子节点间的路径费用小于等于一个定值 \(Value\) 且满足题目中的限制。
发现一条边不能经过两次,显然当我们进入一棵子树时,必须要把子树中所有的叶子节点全部遍历后才能离开这棵子树。
考虑判定性相关的 dp ,设 \(f_u(a,b)\) 表示当前节点 \(u\) 的子树中是否存在一种方案:
-
从 \(u\) 出发到第一个叶子节点的路径长度为 \(a\) ,从 \(u\) 出发到最后一个叶子节点的路径长度为 \(b\)。
-
所有的路径长度都不大于 \(Value\)。
-
在满足所有以上限制的情况下,遍历完 \(u\) 的整棵子树。
对于这个 dp,可以考虑从 \(u\) 的左儿子和右儿子处进行转移。
设 \(u\) 的左儿子为 \(lson_u\),\(u\) 到左儿子的距离为 \(val_l\),右儿子为 \(rson_u\) ,\(u\) 到右儿子的距离为 \(val_r\)。
有转移式 \(f_u(a,b)=f_{lson_u}(a,i) \ \& \ f_{rson_u}(j,b) \ (i+j+val_l+val_r \le Value)\)
(对转移式的解释:从 \(u\) 出发到 \(lson_u\) 的第一个叶子节点,从该叶子节点到 \(lson_u\) 的最后一个叶子节点,从该叶子节点出发到 \(rson_u\) 的第一个叶子节点)
复杂度爆炸,考虑怎么优化这个 dp。
发现当 \(f_u(a_1,b_1)\) 和 \(f_u(a_2,b_2)\) 满足 \(a_1\le a_2,b_1\le b_2\) 的时候,\(f_u(a_2,b_2)\) 是显然不必要存在的。
考虑把 \(f_u\) 的状态按 \(a\) 排序,根据上面的性质,排序后的 \(f_u\) 状态在 \(b\) 递减时是最少的。
显然,对于每一个 \(f_{lson_u}(a_1,b_1)\) 都只需要考虑一个令 \(b_2\) 最小且满足转移式的 \(f_{rson_u}(a_2,b_2)\)。所以,每一个 \(f_{lson_u}\) 对应的状态只需要与一个对应的 \(f_{rson_u}\) 的状态转移到 \(f_u\) 上,每次转移增加的状态数是 \(2\times \min(x,y)\) (\(x\) 是 \(f_{lson_u}\) 的状态数,\(y\) 是 \(f_{rson_u}\) 的状态数,注意 \(lson_u\) 和 \(rson_u\) 是可以反过来再转移一次的),显然状态总数是 \(n \log n\) 的。
至于如何找到这个最小的 \(b_2\),直接 two-points 即可。(至于为什么可以 two-points 是因为 \(a\) 递增而 \(b\) 递减)
时间复杂度 \(\mathcal{O}(n \log^2 n \log v)\),其中 \(v\) 是二分答案中 \(r\) 的权值。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<ll,ll>
#define fi first
#define se second
typedef long long ll;
const int N=2e5+5;
int ch[N][2],val[N][2];
vector<pii> v[N];
void dfs(int x,ll Value){
v[x].clear();
if(!ch[x][0]) return (void)(v[x].pb({0,0}));
for(int i=0;i<2;++i)
if(ch[x][i]) dfs(ch[x][i],Value);
vector<pii> vec;
for(int dy=0;dy<2;++dy){
int ls=ch[x][0^dy],rs=ch[x][1^dy];
ll tmp=Value-val[x][0]-val[x][1];
for(int i=0,j=0;i<v[ls].size();++i){
while(j+1<v[rs].size() && v[rs][j+1].fi<=tmp-v[ls][i].se) ++j;
if(j>=v[rs].size() || v[rs][j].fi>tmp-v[ls][i].se) continue;
vec.pb({v[ls][i].fi+val[x][0^dy],v[rs][j].se+val[x][1^dy]});
}
}
sort(vec.begin(),vec.end());
for(int i=0;i<vec.size();++i){
if(!v[x].empty() && v[x].back().se<=vec[i].se) continue;
v[x].pb(vec[i]);
}
}
bool check(ll mid){
dfs(1,mid);
return v[1].size()>=1;
}
signed main(){
int n,fa,Val;
cin>>n;
ll l=0,r=0,ans=0,mid;
for(int i=2;i<=n;++i){
cin>>fa>>Val;
r+=Val;
if(ch[fa][0]) ch[fa][1]=i,val[fa][1]=Val;
else ch[fa][0]=i,val[fa][0]=Val;
}
while(l<=r){
mid=(l+r)/2;
if(check(mid)) r=mid-1,ans=mid;
else l=mid+1;
}
cout<<ans;
return 0;
}
t1
bfs 跑出来第一天的答案,剩下每个点 \(O(1)\) 计算,注意特判即可。
t2
挺好一道题,第三个包确实难,交互库怎么要用主席树实现。
对于 \(1≤n≤10^3,1≤Q≤10^4\):
应该是 \(n\log n\) 左右的东西。考虑从左到右扫,那么到这个点时,前面不考虑这个点的匹配有多少个颜色已经处理好了,那么二分找出第一个颜色相同的就可以了,代码实现有点难。
对于 \(1≤n,Q≤10^3\),保证颜色段连续:
水包,考虑双指针。
\(1≤n≤10^4,1≤Q≤2\times 10^4\),保证至多有 \(4\) 种不同的颜色 或者 至少也有 \(n−1\) 种不同的颜色:
如果均异色随便做,如果有两个同色就直接二分存在点同色的最大左端点和最小右端点,然后就可以得出同色点对,至多询问 \(2\log n\) 次,可以通过,考虑第一种情况。
考虑维护 \(lst_{col}\),然后你分类讨论,可以证明访问数不超过 \(2n\)。
t3
OI 一定不能考纯数学题!
给定常数 \(T\) 和 \(n\) 个点的 \(t_i\),其坐标为 \((\cos(\dfrac{2 \pi t_i}{T}),\sin(\dfrac{2 \pi t_i}{T}))\),求任取三点得到的三角形的内心期望横坐标和纵坐标,\(t_i\) 严格不同。
记 \(a_i = \dfrac{2 \pi t_i}{T}\),然后排序。
不妨设 \(a_1 \lt a_2 \lt a_3\),则有内心横坐标为 \(\cos(t_1+t_2)+\cos(t_2+t_3)-\cos(t_1+t_3)\),纵坐标为 \(\sin(t_1+t_2)+\sin(t_2+t_3)-\sin(t_1+t_3)\)。
我学过都不知道咋来的。但是大概是用角平分线的性质暴算出来的。
然后用和角公式直接加起来维护一些东西就好了,用线段树区间加。
t4
诈骗题,就是删掉最小的点,使其不存在 \((奇,偶) \to (偶,偶) \to (偶,奇) \to (奇,奇)\) 这样的路径,考虑拆点然后按照这样的路径连边。然后将源点向 \((奇,偶)\) 的点连正无穷的边,将 \((奇,奇)\) 点的向汇点连正无穷的边,对于满足上述路径的点对将他们的出点和入点对应连正无穷的边,跑最大流即可。
P4899
最开始读错题了,以为还有一个部分点不可到达的限制,倒闭了。
读对之后又被自己唐唐的重构树理解做局了。
肯定是做两个重构树,一个最大生成树,一个最小生成树。
考虑有什么性质,就是你可以从 \(s\) 倍增找到第一步的限制点,那么这个子树里的点都是 \(s\) 可以到达的。\(t\) 同理。
所以你找的就是两个子树之间有没有交。考虑 \(dfs\) 序。满足如下条件:
考虑在线离线都是二维数点,直接用 bit 维护就好啦。
P7424
挺板子的,想到对于每个木板单独计算贡献接下来就会想到整体二分/树套树。用 BIT 做前缀和就好了。

浙公网安备 33010602011771号