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;
}

rabbit_mygo 10.25

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\) 序。满足如下条件:

\[L_1\le a_i\le R_1,L_2\le b_i\le R_2 \]

考虑在线离线都是二维数点,直接用 bit 维护就好啦。

P7424

题面

挺板子的,想到对于每个木板单独计算贡献接下来就会想到整体二分/树套树。用 BIT 做前缀和就好了。

posted @ 2025-10-28 20:38  NeeDna  阅读(10)  评论(0)    收藏  举报