THUSC2021 题解

自闭记

T1 move

Description

给定一个长度为 \(n\) 的序列 \(a\) 和一个正整数 \(m\),现在按下述策略删除 \(a\) 中的数字:

选出一个下标序列 \(p\),使得其:

  1. 单调递增
  2. \(\sum_{x\in p}a_x \le m\)
  3. 元素个数最多
  4. 字典序是满足上述条件的序列中最大的

然后删除 \(a\) 中下标在 \(p\) 中的元素。问按上述策略进行几次删除会使 \(a\) 为空。

\(n\le 5\times 10^4,m\le 10^9\)

Solution

如果没有第4条限制,那么只需要用 \(set\) 维护所有数字,然后贪心选择最小的元素即可。如果有第4条限制,通过这样方法得到的元素个数也一定是最多的,因此一定选出的下标序列大小已经确定。

考虑从前往后确定该下标序列中的元素,对于当前位置,考虑二分,那么元素 \(id\) 能放在当前位置,当且仅当下标 \(\ge id\) 且还存在的元素中,最小的 \(k\) 个元素之和 \(\le m\)。 找到 \(id\) 之后,将 \(id\) 删掉,继续二分下一个元素。因此需要支持单点修改,考虑使用树套树维护这个东西,外层用树状数组或线段树维护下标区间,内层用权值线段树维护当前区间的元素的权值。

单点修改时,在 \(\log\) 个区间内同时修改。查询时,将询问拆为 \(\log\) 个区间,然后将这 \(\log\) 个区间的权值线段树合并进行查询。当然事实上你不需要合并,只需要同时维护 \(\log\) 个根节点,正常权值线段树找左子树大小时,就将这 \(\log\) 个根的左子树大小加起来即可。这是树套树的一种常见套路,全世界大概只有我一个不会。

总复杂度为 \(\mathcal O(n\log^3 n)\)

Code

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10,lg=16;
typedef long long ll;
const int inf=0x3f3f3f3f;
int n,m,a[N],ans,b[N],pos[N],cnt;

namespace iobuff{
	const int LEN=1000000;
	char in[LEN+5],out[LEN+5];
	char *pin=in,*pout=out,*ed=in,*eout=out+LEN;
	inline char gc(void){
		#ifdef LOCAL
		return getchar();
		#endif
		return pin==ed&&(ed=(pin=in)+fread(in,1,LEN,stdin),ed==in)?EOF:*pin++;
	}
	inline void pc(char c){
		pout==eout&&(fwrite(out,1,LEN,stdout),pout=out);
		(*pout++)=c;
	}
	inline void flush(){fwrite(out,1,pout-out,stdout),pout=out;}
	template<typename T> inline void read(T &x){
		static int f;
		static char c;
		c=gc(),f=1,x=0;
		while(c<'0'||c>'9') f=(c=='-'?-1:1),c=gc();
		while(c>='0'&&c<='9') x=10*x+c-'0',c=gc();
		x*=f;
	}
	template<typename T> inline void putint(T x,char div){
		static char s[15];
		static int top;
		top=0;
		x<0?pc('-'),x=-x:0;
		while(x) s[top++]=x%10,x/=10;
		!top?pc('0'),0:0;
		while(top--) pc(s[top]+'0');
		pc(div);
	}
}
using namespace iobuff;

int q[N];
namespace SGT{
	const int M=N*lg*lg;
	#define mid ((l+r)>>1)
	int ls[M],rs[M],siz[M],tot;
	ll sum[M];
	inline void update(int &p,int x,int tp,int l=1,int r=cnt){
		if(!p) p=++tot;
		siz[p]+=tp;sum[p]+=tp*b[x];
		if(l==r) return ;
		if(x<=mid) update(ls[p],x,tp,l,mid);
		else update(rs[p],x,tp,mid+1,r);
	}
	inline ll query(int tot,int k,int m,int l=1,int r=cnt){
		int sz=0,lsiz=0;ll lsum=0;
		for(int i=1;i<=tot;++i){
			int p=q[i];
			sz+=siz[p],lsiz+=siz[ls[p]],lsum+=sum[ls[p]];
		}
		if(sz<k) return m+1;
		if(l==r) return 1ll*k*b[l];
		if(lsiz>=k){
			for(int i=1;i<=tot;++i) q[i]=ls[q[i]];
			return query(tot,k,m,l,mid);
		} 
		else{
			if(lsum>m) return m+1;
			for(int i=1;i<=tot;++i) q[i]=rs[q[i]];
			return lsum+query(tot,k-lsiz,m-lsum,mid+1,r);
		}
	}
	#undef mid
}

namespace BIT{
	int rt[N];
	inline int lowbit(int x){return x&(-x);}
	inline void update(int x,int v,int tp){
		for(;x;x-=lowbit(x)) SGT::update(rt[x],v,tp);
	}
	inline ll query(int x,int v,int m){
		int tot=0;
		for(;x<=n;x+=lowbit(x)) q[++tot]=rt[x];
		return SGT::query(tot,v,m);
	}
}

multiset<int> s;
int main(){
//	freopen("1.in","r",stdin); 
	read(n);read(m);
	for(int i=1;i<=n;++i) read(a[i]),b[i]=a[i];
	sort(b+1,b+n+1);
	cnt=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;++i) pos[i]=lower_bound(b+1,b+cnt+1,a[i])-b;
	for(int i=1;i<=n;++i) BIT::update(i,pos[i],1),s.insert(a[i]);
	while(s.size()){
		vector<int> dec,rel;
		int rec=m;ans++;
		while(s.size()&&(*s.begin())<=rec){
			dec.push_back(*s.begin());
			rec-=dec.back();s.erase(s.begin());
		}
		int sum=dec.size();rec=m;
		for(int i=1;i<=sum;++i){
			int l=1,r=n-(sum-i+1)+1,ans=0;
			while(l<=r){
				int mid=(l+r)>>1;
				if(BIT::query(mid,sum-i+1,rec)<=rec) l=mid+1,ans=mid;
				else r=mid-1;
			}
			BIT::update(ans,pos[ans],-1);
			rec-=a[ans];
			rel.push_back(a[ans]);	
		}
		for(int v:dec) s.insert(v);
		for(int v:rel) s.erase(s.find(v));
	}
	printf("%d\n",ans);
	return 0;
}

T2 watermelon

Description

给出一棵树,结点有点权,求所有树上简单路径的最长上升子序列的最大值,\(n\le 10^5,a_i\le 10^9\)

Solution

考虑 \(DP\),注意到一个简单路径可以被拆为向上的部分和向下的部分。所以设 \(f_{u,i}\) 表示 \(u\) 的子树中从 \(u\) 向下且第一项是 \(i\) 的 LIS 的最大长度,\(g_{u,i}\) 表示 \(u\) 的子树中 \(u\) 的某个子孙向上到 \(u\) 且最后一项是 \(i\) 的 LIS 的最大长度。

\(u\) 到父亲 \(fa\) ,转移考虑将 \(a_{fa}\) 作为 \(LIS\) 的开头或结尾:

\[\max_{i=a_{fa}+1}^{n}f_{u,i}+1\rightarrow f_{fa,a_{fa}}\\ \max_{i=1}^{a_{fa}-1}g_{u,i}+1\rightarrow g_{fa,a_{fa}}\\ \]

于是可以对每个节点用线段树维护 \(f\)\(g\),从 \(u\) 转移到 \(fa\) 只需要先将 \(u\) 的线段树用上面的转移方程进行修改,再直接合并到 \(fa\) 的线段树上即可。统计答案时,考虑在合并 \(u\) 之前(此时 \(fa\) 的线段树维护的线段树维护的是只考虑从前几个儿子上来的 LIS 时的 \(f\)\(g\) )更新答案,有 \(f_{u,i}+\max_{j<i} g_{fa,j}\rightarrow ans\)\(g_{fa,i}+\max_{j<i} g_{u,j}\rightarrow ans\)。实际实现时,同时从 \(u\)\(fa\) 的线段树向下走,每次用 $\max f_{lson[fa]}+\max g_{rson[u]} $ 更新答案即可,这是经典的线段树合并套路。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;

struct node{
	int v,nxt;
}e[N<<1];
int cnt,first[N],pos[N],tot,buc[N],ans;
inline void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=first[u];first[u]=cnt;}
namespace SGT{
	const int M=N<<7;
	#define mid ((l+r)>>1)
	int rtf[N],rtg[N];
	int ls[M],rs[M],mx[M],del[M],top,sum;
	inline int newnode(){return top?del[top--]:++sum;}
	inline void dele(int p){
		ls[p]=rs[p]=mx[p]=0;
		del[++top]=p;
	}
	inline int getmx(int p,int ql,int qr,int l=1,int r=tot){
		if(ql<=l&&r<=qr) return mx[p];
		if(!p) return 0;
		int ans=0;
		if(ql<=mid) ans=max(ans,getmx(ls[p],ql,qr,l,mid));
		if(qr>mid) ans=max(ans,getmx(rs[p],ql,qr,mid+1,r));
		return ans;  
	}
	inline void update(int &p,int x,int v,int l=1,int r=tot){
		if(!p) p=newnode();
		if(l==r){mx[p]=max(mx[p],v);return ;}
		if(x<=mid) update(ls[p],x,v,l,mid);
		else update(rs[p],x,v,mid+1,r);
		mx[p]=max(mx[ls[p]],mx[rs[p]]);
	}
	inline int merge(int p1,int p2,int l=1,int r=tot){
		if(!p1||!p2) return p1+p2;
		if(l==r){
			mx[p1]=max(mx[p1],mx[p2]);
			dele(p2);
			return p1;
		} 
		mx[p1]=max(mx[p1],mx[p2]);
		ls[p1]=merge(ls[p1],ls[p2],l,mid);
		rs[p1]=merge(rs[p1],rs[p2],mid+1,r);
		dele(p2);
		return p1;
	}
	inline void query(int p1,int p2,int l=1,int r=tot){
		if(!p1||!p2) return ;
		if(l==r) return ;
		ans=max(ans,mx[ls[p1]]+mx[rs[p2]]);
		query(ls[p1],ls[p2],l,mid);
		query(rs[p1],rs[p2],mid+1,r);
	}
	#undef mid
}
using namespace SGT;

int n,a[N];

inline void work(int u,int f){
	bool flag=0;
	for(int i=first[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v==f) continue;
		work(v,u);
		if(flag) query(rtg[v],rtf[u]);
		if(flag) query(rtg[u],rtf[v]);
		update(rtg[v],pos[u],getmx(rtg[v],1,pos[u]-1)+1);
		update(rtf[v],pos[u],getmx(rtf[v],pos[u]+1,tot)+1);
		rtf[u]=merge(rtf[u],rtf[v]);
		rtg[u]=merge(rtg[u],rtg[v]);
		flag=1;
	}	
	if(!flag) update(rtf[u],pos[u],1),update(rtg[u],pos[u],1);
	ans=max(ans,mx[rtf[u]]);ans=max(ans,mx[rtg[u]]);
}

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),buc[i]=a[i];
	sort(buc+1,buc+n+1);
	tot=unique(buc+1,buc+n+1)-buc-1;
	for(int i=1;i<=n;++i) pos[i]=lower_bound(buc+1,buc+tot+1,a[i])-buc;
	for(int i=1,u,v;i<n;++i) scanf("%d%d",&u,&v),add(u,v),add(v,u);
	work(1,0);
	printf("%d\n",ans);
	return 0;
} 

T3 emiya

Description

\(n\) 个人和 \(m\) 种菜 , 第 \(i\) 个人对第 \(j\) 道菜的喜爱程度为 \(a_{i,j}\), 如果 \(a_{i,j}=−1\)则表示不喜欢 .

现在你要选择一个菜的集合,你会获得喜欢集合中所有菜的人对这些菜的喜爱程度之和的权值,最大化这个权值,\(n\le 20,m\le 10^6,a_{i,j}\le 10^9\)

Solution

考虑求出 \(f_S\) 表示钦定 \(S\) 中的人喜欢所有的菜,不管其他人时,能获得的最大权值。显然答案 \(=\max_S f_S\)

考虑 \(a_{i,j}\) 能为哪些 \(S\) 作出贡献,设 \(t_j\) 为喜欢 \(j\) 的人的集合,那么会受到 \(a_{i,j}\) 贡献的 \(S\) 应当满足 \(S\in t_j,i\in S\)。考虑记 \(g_S\) 表示对 \(S\) 的所有子集一起造成的贡献,即 \(f_S=\sum_{S\in T}g_{T}\)。于是贡献就相当于 \(g_{t_j}+=a_{i,j},g_{t_j\otimes 2^i}-=a_{i,j}\)

求出 \(g\) 后再求 \(f\),直接 \(FWT\)\(FMT\) 求解即可。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=(1<<20)+20; 
int n,m,a[21][N],s[N];
ll f[N];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) 
		for(int j=1;j<=m;++j){
			scanf("%d",&a[i][j]);
			if(a[i][j]!=-1) s[j]|=1<<i-1;
		}
	for(int i=1;i<=n;++i) 
		for(int j=1;j<=m;++j)
			if(a[i][j]!=-1) f[s[j]]+=a[i][j],f[s[j]^(1<<i-1)]-=a[i][j];
	for(int i=0;i<n;++i)
		for(int j=0;j<(1<<n);++j) if(j&(1<<i)) f[j^(1<<i)]+=f[j];
	ll ans=0;
	for(int i=0;i<(1<<n);++i) ans=max(ans,f[i]);
	printf("%lld\n",ans);
	return 0;
}

T4 tree

Description

你需要实现两个函数 \(encode,decode\)

\(encode\) 的功能是接收一个 \(n\) 个点的有根树,返回一个 \(128\) 位二进制数。

\(decode\) 的功能是接收一个 \(128\) 位的二进制数,返回一个有根树。

题目会给你 \(T\) 颗有根树给 \(encode\),把你 \(decode\) 返回的二进制数给第二个函数。

你需要使得第二个函数返回的有根树和给你的有根树同构。

同构指的是将每个节点的儿子以编号大小排序后,两棵树无标号有根同构。

\(n\le 70,T\le 10^5\)

Solution

两棵树同构,其实就相当于,\(dfs\) 遍历整个树时,每次从父亲走向儿子在序列后添一个 \(0\),从儿子走向父亲在序列后添一个 \(1\),两棵树得到的序列相同则这两棵树同构。因此直接传递这个长为 \(2(n-1)\) 的序列就可以完成 \(n=65\) 的情况。

注意到这个 \(01\) 序列如果把 \(0\) 看作左括号,\(1\) 看作右括号,那么原序列就变成了一个合法的括号序列。而长为 \(n\) 的合法括号序列有 \(C(n)\) 个,其中 \(C\) 是卡特兰数,而 \(C(128)<2^{128}\),因此考虑求出每个序列是字典序第几小的合法括号序列,然后就能将每棵树映射为一个合法括号序列了。

考虑求出 \(f_{i,j}\) 表示已经有了 \(i\) 个左括号,\(j\) 个右括号且前半部分保证合法,接下来有多少种放括号的方案使得括号序列合法。计算一个括号序列的字典序时,如果第 \(i\) 个位置是右括号,那么此前位置与序列相同,第 \(i\) 个位置是左括号的序列都比他小,这样的序列有 \(f_{x+1,y}\) 个,其中 \(x,y\) 表示前 \(i-1\) 个位置有 \(x\) 个左括号,\(y\) 个右括号。

于是直接预处理出来每个括号长度对应的 \(f\),总复杂度为 \(\mathcal O(n^3+nT)\)

Code

#include "tree.h"
#include<bits/stdc++.h>
using namespace std; 
#define u128 unsigned __int128
const int N=75;
u128 f[N][N][N];
int vis[N];
inline void init(int n){
	f[n][n][n]=1;
	for(int sum=n<<1;sum>=1;--sum){
		for(int i=min(sum,n);i>=0&&i>=sum-i;--i){ 
			int j=sum-i;
			if(!f[n][i][j]) continue;
			if(i-1>=j&&i) f[n][i-1][j]+=f[n][i][j];
			if(j) f[n][i][j-1]+=f[n][i][j];
		}
	}
}
vector<int> to[N];
int ans[N<<1],top;
inline void dfs(int u,int f){
	for(int v:to[u]){
		if(v==f) continue;
		ans[++top]=0;dfs(v,u);
	}
	if(f) ans[++top]=1;
}
inline void write(u128 M){
	if(M>=10) write(M/10);
	putchar(M%10+'0');
}
u128 encode(int n,const int *p){
	if(!vis[n-1]) vis[n-1]=1,init(n-1);
	for(int i=1;i<=n;++i) to[i].clear();
	for(int i=2;i<=n;++i) to[p[i]].push_back(i);
	top=0;dfs(1,0);
	u128 ret=0;
	for(int i=1,a=0,b=0;i<=top;++i){
		if(ans[i]==1) ret+=f[n-1][a+1][b],b++;
		else a++;
	}
	return ret;
}
int dfn[N<<1];
void decode(int n,u128 M,int *p){
	if(!vis[n-1]) vis[n-1]=1,init(n-1); 
	top=0;p[1]=0;
	for(int i=1,a=0,b=0;i<=(n-1)<<1;++i){
		if(M>=f[n-1][a+1][b]) M-=f[n-1][a+1][b],dfn[i]=1,b++;
		else dfn[i]=0,a++; 
	}
	int now=1,cnt=1;
	for(int i=1;i<=(n-1)<<1;++i){
		if(!dfn[i]){
			++cnt;
			p[cnt]=now;now=cnt;
		}else now=p[now];
	}
}
posted @ 2021-05-18 19:16  cjTQX  阅读(165)  评论(0编辑  收藏  举报