#6 CF571D & CF590E & CF724E

我这进度也太慢了吧,果然我整个人就是一个水。

Campus

题目描述

点此看题

解法

我自己想到正解的题都是水题,这题也不例外

考虑在并查集上修改的主要方法就是在根上打标记,那么本题我们就打标记,并且为了复杂度我们不下放,而是在询问的时候暴力跳父亲来计算标记的影响,前提是启发式合并保证深度是 \(O(\log n)\) 级别。

考虑清除操作(就是整个并查集赋 \(0\))的影响可以通过计算每个点最后一次被清除的时间得到,那么我们在每个节点上维护一个清除标记,在清除时间 \(\geq\) 合并时间的条件下父亲的清除就可以作用于儿子,那么我们可以在线算出清除时间。

对于加法操作我们维护一个以时间为顺序的 \(\tt vector\),那么知道了清除时间之后就可以在上面二分,有贡献的加法标记一定是一段后缀(注意还要把合并时间也考虑进去),时间复杂度 \(O(n\log^2n)\)

总结

数据结构题,不一定要把东西全部维护出来询问直接去拿,询问操作可以承担起在线计算的作用。

观察操作的特性,比如本题清除操作的特性就是爹,清除之后什么都没有了,所以我们只需要知道最后一次清除的时间。

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 500005;
#define pii pair<int,int>
#define x first
#define y second
#define ll long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
void write(ll x)
{
	if(x>=10) write(x/10);
	putchar(x%10+'0');
}
int n,m,k,fa[2][M],t[2][M],sz[2][M],z[M];
vector<int> d[M];vector<ll> s[M];
int find(int x,int o)
{
	return (x!=fa[o][x])?find(fa[o][x],o):x;
}
void merge(int u,int v,int o)
{
	u=find(u,o);v=find(v,o);
	if(u==v) return ;
	if(sz[o][u]<sz[o][v]) swap(u,v);
	fa[o][v]=u;sz[o][u]+=sz[o][v];t[o][v]=k;
}
void add(int x)
{
	x=find(x,0);
	d[x].push_back(k);
	s[x].push_back(s[x].back()+sz[0][x]);
}
void clear(int x)
{
	x=find(x,1);
	z[x]=max(z[x],k);
}
ll ask(int x)
{
	int p=z[x],y=x;ll ans=0;
	//calc the time of lastest clear
	while(y!=fa[1][y])
	{
		int f=fa[1][y];
		if(t[1][y]<=z[f]) p=max(p,z[f]);
		y=f;
	}
	y=x;int ls=0;
	//calc the answer
	while(y)
	{
		int w=lower_bound(d[y].begin(),d[y].end()
		,max(p,ls))-d[y].begin();
		ans+=s[y].back()-s[y][w];
		if(y==fa[0][y]) break;
		ls=t[0][y];y=fa[0][y];
	}
	return ans;
}
signed main()
{
	n=read();m=read();char c[5];
	for(int i=1;i<=n;i++)
	{
		fa[0][i]=fa[1][i]=i,sz[0][i]=sz[1][i]=1;
		s[i].push_back(0);
	}
	for(k=1;k<=m;k++)
	{
		scanf("%s",c);
		if(c[0]=='U') merge(read(),read(),0);
		if(c[0]=='M') merge(read(),read(),1);
		if(c[0]=='A') add(read());
		if(c[0]=='Z') clear(read());
		if(c[0]=='Q') write(ask(read())),puts("");
	}
}

Birthday

题目描述

点此看题

解法

把很多最长反链的题放在一起做了,希望能巩固这个知识点!

考虑本题的限制其实就是满足子串关系的点不能连边(区分 \(\tt 2sat\) 的共存模型),那么我们尝试把这东西建成一个拓扑图,也就是如果 \(y\)\(x\) 的子串那么我们连边 \(x\rightarrow y\)

那么考虑怎么描述子串这东西,我们考虑使用 \(\tt AC\) 自动机,构建出自动机之后我们对于每个串的每个前缀暴力往上 \(\tt fail\),找到所有有后缀关系的节点(对于每个前缀考虑其后缀)

但是暴力 \(\tt fail\) 显然不行,但是如果链上不存在关键点可以把这条 \(\tt fail\) 缩起来,具体方式就是更改 \(\tt fail\) 到向上的第一个关键点。那么每条 \(\tt fail\) 就具有 \(1\) 的势能,所有这部分时间复杂度 \(O(m)\)

我们可以用 \(\tt bitset\) 跑传递闭包,然后暴力二分图匹配,利用经典方法构造答案即可。

易错点:注意区分左右的 \(\tt match\) 节点,跑二分图的时候用的右部 \(\tt match\),但是构造的时候用的是左部 \(\tt match\)

#include <cstdio>
#include <bitset>
#include <vector>
#include <cassert>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int M = 755;
const int N = 10000005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,cnt,c[N][2],fail[N],id[N],fa[N];char s[N];
int ed[M],vis[M],mat[M],tl[M],tr[M];bitset<M> g[M];
void ins(int x)
{
	scanf("%s",s+1);m=strlen(s+1);int p=0;
	for(int i=1;i<=m;i++)
	{
		int o=s[i]-'a';
		if(!c[p][o]) fa[c[p][o]=++cnt]=p;
		p=c[p][o];
	}
	id[p]=x;ed[x]=p;
}
void build()
{
	queue<int> q;
	for(int i=0;i<2;i++)
		if(c[0][i]) q.push(c[0][i]);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=0;i<2;i++)
			if(c[u][i])
			{
				fail[c[u][i]]=c[fail[u]][i];
				q.push(c[u][i]);
			}
			else c[u][i]=c[fail[u]][i];
	}
}
void link(int u)
{
	vector<int> o;
	for(int i=ed[u];i;i=fa[i])
	{
		int x=fail[i];o.clear();
		while(x&&!id[x]) o.push_back(x),x=fail[x];
		while(!o.empty()) fail[o.back()]=x,o.pop_back();
		fail[i]=x;
		if(i!=ed[u] && id[i]) g[u][id[i]]=1;
		else g[u][id[x]]=1;
	}
}
int dfs(int x)
{
	for(int i=1;i<=n;i++)
		if(g[x][i] && !vis[i])
		{
			vis[i]=1;
			if(!mat[i] || dfs(mat[i]))
				{mat[i]=x;return 1;}
		}
	return 0;
}
void work(int u)
{
	tr[u]=1;
	for(int i=1;i<=n;i++)
		if(g[i][u] && !tl[i])
			tl[i]=1,work(vis[i]);
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) ins(i);
	build();
	for(int i=1;i<=n;i++) link(i);
	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			if(g[i][k]) g[i]|=g[k];
	int ans=n;
	for(int i=1;i<=n;i++)
		ans-=dfs(i),memset(vis,0,sizeof vis);
	for(int i=1;i<=n;i++) vis[mat[i]]=i;
	for(int i=1;i<=n;i++) if(!mat[i]) work(i);
	printf("%d\n",ans);
	for(int i=1;i<=n;i++) if(!tl[i] && tr[i])
		printf("%d ",i);
}

Goods transportation

题目描述

点此看题

解法

话说这题本来自己想了一个贪心,细节有点多,但是应该是对的

本题的 货物运输 操作可以很容易地联想到网络流,那么我们把 \(S\) 向每个点连 \(p_i\) 的边,两点 \(i<j\) 之间连接 \(c\) 的边,每个点再向 \(T\) 连接 \(s_i\) 的边,这张图的最大流就是答案。

最大流通常转化为最小割求出,我们考虑规划每个点是属于 \(S\) 还是属于 \(T\),如果属于 \(S\) 那么有 \(s_i\) 的代价,如果属于 \(T\) 会有 \(p_i+j\cdot c\) 的代价,其中 \(j\) 是前面的点属于 \(S\) 的点数。

\(f_{i,j}\) 表示前 \(i\) 个点中有 \(j\) 个属于 \(S\) 的最小代价,转移:

\[f_{i,j}\leftarrow f_{i,j-1}+s_i \]

\[f_{i,j}\leftarrow f_{i-1,j}+p_i+j\cdot c \]

时间复杂度 \(O(n^2)\),其实这种用 \(dp\) 解决最小割的思想是常见的:Breadboard Capacity


可以使用调整法 \(+\) 贪心来进一步的优化,首先我们割掉所有点和 \(S\) 相连的边,让后调整一部分点为和 \(T\) 联通,那么调整的代价是 \(a_i=c\cdot (n-i)+s_i-p_i\),还有就是如果已经调整了 \(x\) 个点,那么加上 \(a_i\) 的同时还要减去 \((x-1)\cdot c\),因为这些边是不需要割掉的,那么我们贪心地从小到大选即可,时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 10005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,c,ans,p[M],s[M],f[M],g[M];
signed main()
{
	n=read();c=read();
	for(int i=1;i<=n;i++) p[i]=read();
	for(int i=1;i<=n;i++) s[i]=read();
	for(int i=1;i<=n;i++)
	{
		swap(f,g);f[0]=g[0]+p[i];f[i]=g[i-1]+s[i];
		for(int j=1;j<i;j++)
			f[j]=min(g[j]+p[i]+j*c,g[j-1]+s[i]);
	}
	printf("%lld\n",*min_element(f,f+1+n));
}
posted @ 2022-02-22 19:25  C202044zxy  阅读(171)  评论(0编辑  收藏  举报