@6 UOJ193 & UOJ119 & UOJ284
人类补完计划
题目描述
无关吐槽:没看过 \(\tt EVA\) 的我,以为这个计划是让我把题都补完,那不是不可能的吗?
解法
太离谱了,搞了差不多有一个下午,最后把我讲懂的竟然是它,以后题解还是要挑短的看。
这题就是要我们对基环树加权计数,首先考虑如何求出点集 \(S\) 为基环树的方案数,分为有叶子和没有叶子两种情况讨论。后者是经典问题,对于每个环钦定标号最小的点为起点,然后跑状压 \(dp\) 即可。
对于前者,考虑类似 \(\tt DAG\) 计数的方法,我们可以容斥叶子集合 \(T\),容斥系数设置为 \(-1^{|T|+1}\),设 \(h_i\) 表示点集 \(i\) 的基环树个数,初始化 \(h_i\) 为点集 \(i\) 的环个数,然后用刷表法转移:
其中 \(way(j,i)\) 表示点集 \(j\) 中每个点恰好向点集 \(i\) 中的每个点连一条边的方案树,是单点到 \(i\) 边数的乘积。那么我们可以在 \(O(3^n)\) 的时间内得到点集 \(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\) 的答案是:
我们预处理出单调栈中的前缀和,每次询问时二分找到第一个大于等于 \(\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]);
}