@6 UOJ193 & UOJ119 & UOJ284

人类补完计划

题目描述

点此看题

无关吐槽:没看过 \(\tt EVA\) 的我,以为这个计划是让我把题都补完,那不是不可能的吗?

解法

太离谱了,搞了差不多有一个下午,最后把我讲懂的竟然是以后题解还是要挑短的看

这题就是要我们对基环树加权计数,首先考虑如何求出点集 \(S\) 为基环树的方案数,分为有叶子和没有叶子两种情况讨论。后者是经典问题,对于每个环钦定标号最小的点为起点,然后跑状压 \(dp\) 即可。

对于前者,考虑类似 \(\tt DAG\) 计数的方法,我们可以容斥叶子集合 \(T\),容斥系数设置为 \(-1^{|T|+1}\),设 \(h_i\) 表示点集 \(i\) 的基环树个数,初始化 \(h_i\) 为点集 \(i\) 的环个数,然后用刷表法转移:

\[h_{i\or j}\leftarrow (-1)^{|j|+1}\cdot h_i\cdot way(j,i) \]

其中 \(way(j,i)\) 表示点集 \(j\) 中每个点恰好向点集 \(i\) 中的每个点连一条边的方案树,是单点到 \(i\) 边数的乘积。那么我们可以在 \(O(3^n)\) 的时间内得到点集 \(i\) 的基环树个数。

考虑计算答案,权值的组合意义是基环树的染色方案数,限制是叶子节点只能染白色。那么我们枚举黑色的叶子集合来容斥,其他点可以任意染色:

\[ans=\sum _{i} \sum_{i\subseteq j} (-1)^{|j|-|i|}\cdot h_i\cdot 2^i\cdot way(j-i,i) \]

总时间复杂度 \(O(3^n)\)

总结

第一步用到的容斥思想:从一种错误的计数方法出发,配凑容斥系数使得计数正确。

第二步用到的容斥思想:直接对关键性质容斥。

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1<<16;
const int MOD = 998244353;
#define pc(x) __builtin_popcount(x)
#define ct(x) __builtin_ctz(x)
#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,m,ans,g[16],f[M][16],h[M],w[M];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=m;i++)
	{
		int u=read()-1,v=read()-1;
		g[u]|=1<<v;g[v]|=1<<u;
	}
	for(int i=0;i<n;i++) f[1<<i][i]=1;
	for(int i=0;i<(1<<n);i++)
	{
		int t=0;
		for(int j=0;j<n;j++) if(i>>j&1)
		{
			for(int k=ct(i)+1;k<n;k++)
				if(!(i>>k&1) && (g[j]>>k&1))
					add(f[i|(1<<k)][k],f[i][j]);
			if(pc(i)>2 && (g[ct(i)]>>j&1))
				add(t,f[i][j]);
		}
		add(h[i],(MOD+1)/2*t%MOD);w[0]=h[i];
		add(ans,h[i]<<pc(i));
		for(int j=(i+1)|i;j<(1<<n);j=(j+1)|i)
		{
			int k=j-i;
			w[k]=w[k-(k&(-k))]*pc(g[ct(k)]&i)%MOD;
			add(h[j],pc(k)%2?w[k]:MOD-w[k]);
			add(ans,(pc(k)%2?MOD-w[k]:w[k])<<pc(i));
		}
	}
	printf("%lld\n",ans);
}

决战圆锥曲线

题目描述

点此看题

解法

\(f(x,y)=ax+by+cxy\),那么如果 \(x_1\leq x_2,y_1\leq y_2\),则有 \(f(x_1,y_1)\leq f(x_2,y_2)\)

考虑利用数据随机的性质,对于询问我们直接在线段树上搜索。假设现在进入了区间 \([l,r]\),其最大值是 \(mx_i\),类似 kd-tree 的判断方法,如果 \(f(r,mx_i)\leq ans\),那么直接退出这个区间;否则先递归右儿子,再递归左儿子

考虑这样被搜到的点 \(y\) 一定是递增的,也就是构成了一个从后往前的极长上升子序列。由于数据随机,极长上升子序列的期望是 \(O(\log n)\) 的,所以询问的时间复杂度 \(O(n\log^2 n)\)

修改时平凡的,直接维护即可,时间复杂度 \(O(q\log n)\)

总结

先递归右子树,再递归左子树是很厉害的:Souvenirs虽然这两道题做法大不相同,但还是可以结合起来理解

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
#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,m,x,a,b,c,ans,mx[M<<2],mi[M<<2],fl[M<<2];
int random(int p)
{
	x=(100000005*x+20150609)%998244353;
	return (x/100)%p;
}
void up(int i)
{
	mi[i]=min(mi[i<<1],mi[i<<1|1]);
	mx[i]=max(mx[i<<1],mx[i<<1|1]);
}
void flip(int i)
{
	fl[i]^=1;
	mx[i]=100000-mx[i];
	mi[i]=100000-mi[i];
	swap(mx[i],mi[i]);
}
void down(int i)
{
	if(!fl[i]) return ;
	flip(i<<1);flip(i<<1|1);fl[i]=0;
}
void ins(int i,int l,int r,int id,int c)
{
	if(l==r) {mx[i]=mi[i]=c;return ;}
	int mid=(l+r)>>1;down(i);
	if(mid>=id) ins(i<<1,l,mid,id,c);
	else ins(i<<1|1,mid+1,r,id,c);
	up(i);
}
void work(int i,int l,int r,int L,int R)
{
	if(L>r || l>R) return ;
	if(L<=l && r<=R) {flip(i);return ;}
	int mid=(l+r)>>1;down(i);
	work(i<<1,l,mid,L,R);
	work(i<<1|1,mid+1,r,L,R);
	up(i);
}
void zxy(int i,int l,int r,int L,int R)
{
	if(L>r || l>R) return ;
	if(L<=l && r<=R)
	{
		int t=a*r+b*mx[i]+c*r*mx[i];
		if(t<=ans) return ;
		if(l==r) {ans=t;return ;}
	}
	int mid=(l+r)>>1;down(i);
	zxy(i<<1|1,mid+1,r,L,R);
	zxy(i<<1,l,mid,L,R);
	up(i);
}
signed main()
{
	n=read();m=read();x=read();
	for(int i=1;i<=n;i++)
		ins(1,1,n,i,random(100001));
	while(m--)
	{
		static char s[5];scanf("%s",s);
		int l=random(n)+1;
		if(s[0]=='C')
		{
			ins(1,1,n,l,random(100001));
			continue;
		}
		int r=random(n)+1;
		if(l>r) swap(l,r);
		if(s[0]=='R') work(1,1,n,l,r);
		else
		{
			a=read(),b=read(),c=read();
			ans=0;zxy(1,1,n,l,r);
			printf("%lld\n",ans);
		}
	}
}

快乐游戏鸡

题目描述

点此看题

解法

首先考虑序列上的情况,此时唯一的策略就是直接从 \(s\) 撞到 \(t\),并且我们发现,如果 \(i<j\) 并且 \(w_i\geq w_j\),那么 \(j\) 是没有用的。排除掉这些无用的 \(j\) 之后,剩下的元素构成单调栈。

可以从后到前扫描,动态地插入元素,维护一个 \(w\) 单调递减的单调栈。设栈中的元素分别为 \(d_1,d_2...d_k\),对应的阈值为 \(w_1,w_2...w_k\),那么 \(s\rightarrow t\) 的答案是:

\[(t-s)+\sum_{i=1}^k d_i\cdot (w_i-w_{i-1}) \]

我们预处理出单调栈中的前缀和,每次询问时二分找到第一个大于等于 \(\max_{i=s}^t w_{i}\) 的元素,然后就可以计算了。


把上面的做法搬到树上,我们只需要贪心地找到最近的增大死亡次数的点,然后走到它就可以增加死亡次数。还是可以用单调栈维护这东西,只不过我们按照和 \(s\) 的距离从大到小地插入元素,维护 \(w\) 单调递减的单调栈

要得到每个点的单调栈,可以考虑启发式合并。其实可以直接套用长链剖分的方法,因为单调栈的大小最大是子树的深度。类似归并排序就可以合并两个单调栈,所以得到单调栈的时间复杂度 \(O(n)\)

询问仍然套用序列上的方法,总时间复杂度 \(O(n+q\log n)\)

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 300005;
#define pb push_back
#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;
}
int n,m,w[M],fa[M],md[M],d[M],f[M];ll ans[M];
struct node{ll s;int d,w;};
vector<node> s[M],s1,s2,q[M];vector<int> g[M];
void ins(vector<node> &s,node x)
{
	while(!s.empty() && s.back().w<=x.w)
		s.pop_back();
	x.s=s.empty()?0:s.back().s+1ll*
		s.back().d*(s.back().w-x.w);
	s.pb(x);
}
int find(int x)
{
	if(x!=f[x])
	{
		int t=find(f[x]);
		w[x]=max(w[x],w[f[x]]);f[x]=t;
	}
	return f[x];
}
void dfs(int u)
{
	int son=0;
	for(int v:g[u])
	{
		md[v]=d[v]=d[fa[v]=u]+1;
		dfs(v);md[u]=max(md[u],md[v]);
		if(md[v]>md[son]) son=v;
	}
	swap(s[u],s[son]);
	for(int v:g[u]) if(v^son)
	{
		while(!s[u].empty() && s[u].back().d<s[v][0].d)
			s1.pb(s[u].back()),s[u].pop_back();
		s2.resize(s1.size()+s[v].size());
		reverse(s1.begin(),s1.end());
		merge(s1.begin(),s1.end(),s[v].begin(),s[v].end(),
		s2.begin(),[&](node x,node y){return x.d>y.d;});
		for(auto x:s2) ins(s[u],x);
		s1.clear();s2.clear();
	}
	ins(s[u],node{0,0,0});
	for(auto x:q[u])
	{
		int v=x.d,id=x.w,c=0;ans[id]+=d[v]-d[u];
		if(d[v]-d[u]<=1) continue;
		find(fa[v]);c=w[fa[v]];
		int l=0,r=s[u].size()-1,p=0;
		while(l<=r)
		{
			int mid=(l+r)>>1;
			if(s[u][mid].w>=c) p=mid,l=mid+1;
			else r=mid-1;
		}
		ans[id]+=s[u].back().s-s[u][p+1].s+
		1ll*(c-s[u][p+1].w)*s[u][p].d-1ll*d[u]*c;
	}
	ins(s[u],node{0,d[u],w[u]});f[u]=u;
	for(int v:g[u]) f[v]=u;
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) w[i]=read();
	for(int i=2;i<=n;i++) g[read()].pb(i);
	m=read();
	for(int i=1;i<=m;i++)
	{
		int s=read(),t=read();
		q[s].pb(node{0,t,i});
	}
	dfs(1);
	for(int i=1;i<=m;i++)
		printf("%lld\n",ans[i]);
}
posted @ 2022-07-17 16:31  C202044zxy  阅读(242)  评论(3编辑  收藏  举报