T1 move
Description
给定一个长度为 (n) 的序列 (a) 和一个正整数 (m),现在按下述策略删除 (a) 中的数字:
选出一个下标序列 (p),使得其:
- 单调递增
- (sum_{xin p}a_x le m)
- 元素个数最多
- 字典序是满足上述条件的序列中最大的
然后删除 (a) 中下标在 (p) 中的元素。问按上述策略进行几次删除会使 (a) 为空。
(nle 5 imes 10^4,mle 10^9)。
Solution
如果没有第4条限制,那么只需要用 (set) 维护所有数字,然后贪心选择最小的元素即可。如果有第4条限制,通过这样方法得到的元素个数也一定是最多的,因此一定选出的下标序列大小已经确定。
考虑从前往后确定该下标序列中的元素,对于当前位置,考虑二分,那么元素 (id) 能放在当前位置,当且仅当下标 (ge id) 且还存在的元素中,最小的 (k) 个元素之和 (le m)。 找到 (id) 之后,将 (id) 删掉,继续二分下一个元素。因此需要支持单点修改,考虑使用树套树维护这个东西,外层用树状数组或线段树维护下标区间,内层用权值线段树维护当前区间的元素的权值。
单点修改时,在 (log) 个区间内同时修改。查询时,将询问拆为 (log) 个区间,然后将这 (log) 个区间的权值线段树合并进行查询。当然事实上你不需要合并,只需要同时维护 (log) 个根节点,正常权值线段树找左子树大小时,就将这 (log) 个根的左子树大小加起来即可。这是树套树的一种常见套路,全世界大概只有我一个不会。
总复杂度为 (mathcal O(nlog^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
",ans);
return 0;
}
T2 watermelon
Description
给出一棵树,结点有点权,求所有树上简单路径的最长上升子序列的最大值,(nle 10^5,a_ile 10^9)。
Solution
考虑 (DP),注意到一个简单路径可以被拆为向上的部分和向下的部分。所以设 (f_{u,i}) 表示 (u) 的子树中从 (u) 向下且第一项是 (i) 的 LIS 的最大长度,(g_{u,i}) 表示 (u) 的子树中 (u) 的某个子孙向上到 (u) 且最后一项是 (i) 的 LIS 的最大长度。
从 (u) 到父亲 (fa) ,转移考虑将 (a_{fa}) 作为 (LIS) 的开头或结尾:
于是可以对每个节点用线段树维护 (f) 和 (g),从 (u) 转移到 (fa) 只需要先将 (u) 的线段树用上面的转移方程进行修改,再直接合并到 (fa) 的线段树上即可。统计答案时,考虑在合并 (u) 之前(此时 (fa) 的线段树维护的线段树维护的是只考虑从前几个儿子上来的 LIS 时的 (f) 与 (g) )更新答案,有 (f_{u,i}+max_{j<i} g_{fa,j} ightarrow ans),(g_{fa,i}+max_{j<i} g_{u,j} ightarrow 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
",ans);
return 0;
}
T3 emiya
Description
有 (n) 个人和 (m) 种菜 , 第 (i) 个人对第 (j) 道菜的喜爱程度为 (a_{i,j}), 如果 (a_{i,j}=−1)则表示不喜欢 .
现在你要选择一个菜的集合,你会获得喜欢集合中所有菜的人对这些菜的喜爱程度之和的权值,最大化这个权值,(nle 20,mle 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) 应当满足 (Sin t_j,iin S)。考虑记 (g_S) 表示对 (S) 的所有子集一起造成的贡献,即 (f_S=sum_{Sin T}g_{T})。于是贡献就相当于 (g_{t_j}+=a_{i,j},g_{t_jotimes 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
",ans);
return 0;
}
T4 tree
Description
你需要实现两个函数 (encode,decode)。
(encode) 的功能是接收一个 (n) 个点的有根树,返回一个 (128) 位二进制数。
(decode) 的功能是接收一个 (128) 位的二进制数,返回一个有根树。
题目会给你 (T) 颗有根树给 (encode),把你 (decode) 返回的二进制数给第二个函数。
你需要使得第二个函数返回的有根树和给你的有根树同构。
同构指的是将每个节点的儿子以编号大小排序后,两棵树无标号有根同构。
(nle 70,Tle 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];
}
}