#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\) 的最小代价,转移:
时间复杂度 \(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));
}