Huffman Coding on Segment
题目描述
解法
算法都写题目名称里了,就是对原序列的每个区间做哈夫曼编码,哈夫曼树的构造方式如下:
while(q.size()>1)
{
int x=q.top();q.pop();
x+=q.top();q.pop();
q.push(x);res+=x;
}
简单理解就是对区间做合并果子,值得注意的是优先队列里的元素为频率(即这个字符的出现次数)
既然是涉及到频率的问题,根号分治是一个很好的方向,对于频率 \(>\sqrt n\) 的字符,可以直接暴力优先队列。
对于频率 \(\leq \sqrt n\) 的字符,我们可以用莫队统计频率的出现次数,然后可以按照值域扫描频率,记录个数来模拟合并的过程即可,如果合并出来的结果 \(>\sqrt n\) 就丢到优先队列里面。
时间复杂度 \(O(n\sqrt n\log n)\),调调阈值就可以做到 \(O(n\sqrt {n\log n})\),
看来本题就是 合并果子 和 合并果子加强版 两道题拼起来的合并题目。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <queue>
using namespace std;
const int M = 100005;
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,k,q,a[M],b[M],c[M],f[M],ans[M],cnt[M];
struct node
{
int l,r,id;
bool operator < (const node &b) const
{
return l/m==b.l/m?r<b.r:l<b.l;
}
}s[M];
void add(int x,int c)
{
cnt[f[x]]--;f[x]+=c;cnt[f[x]]++;
}
int solve()
{
int res=0,j=0;
priority_queue<int,vector<int>,greater<int> > q;
for(int i=1;i<=k;i++)
if(f[b[i]]>m) q.push(f[b[i]]);
for(int i=1;i<=m;i++) c[i]=cnt[i];
for(int i=1;i<=m;i++) if(c[i])
{
if(j)
{
if(i+j>m) q.push(i+j);
else c[i+j]++;
c[i]--;res+=i+j;j=0;
}
if(c[i]%2) j=i,c[i]--;
res+=i*c[i];
if(2*i>m)
for(int k=1;k<=c[i]/2;k++) q.push(i<<1);
else c[i<<1]+=c[i]/2;
}
if(j) q.push(j);
while(q.size()>1)
{
int x=q.top();q.pop();
x+=q.top();q.pop();
q.push(x);res+=x;
}
return res;
}
signed main()
{
n=read();m=sqrt(n);
for(int i=1;i<=n;i++)
f[a[i]=read()]++;
for(int i=1;i<M;f[i++]=0)
if(f[i]>m) b[++k]=i;
q=read();
for(int i=1;i<=q;i++)
s[i].l=read(),s[i].r=read(),s[i].id=i;
sort(s+1,s+1+q);
for(int i=1,l=1,r=0;i<=q;i++)
{
int L=s[i].l,R=s[i].r,id=s[i].id;
while(L<l) add(a[--l],1);
while(r<R) add(a[++r],1);
while(l<L) add(a[l++],-1);
while(R<r) add(a[r--],-1);
ans[id]=solve();
}
for(int i=1;i<=q;i++)
printf("%d\n",ans[i]);
}
Appleman and a Game
题目描述
解法
使我感受到了思维上的洗礼,真是一道不可多得的好题。
首先考虑如果目标串 \(s\) 给定,我们如何计算最小操作数?显然的思路是把 \(s\) 放在 \(t\) 的后缀自动机上匹配,如果新加入字符 \(c\) 之后使得当前的状态不是子串,那么直接回到根节点,并且增加 \(1\) 的操作次数。
根据上述方法可以轻易地写出 \(dp\) 式子,设 \(f_{i,j}\) 表示现在 \(s\) 的长度为 \(i\),匹配到了后缀自动机上的节点 \(s\) 的操作次数。转移可以枚举新插入的字符 \(c\),然后考虑状态 \(j\) 的变化。
但是这样做复杂度直接上天,而且似乎没有任何优化的余地,那么我们就要从源头解决问题,使用 增大转移跨度 的技巧,我们考虑计算操作次数为 \(k\) 时,能解决长度 \(x\) 以内的所有字符串,这个 \(x\) 最大是多少。
但是新问题和原问题毕竟有一些出入,我们可以用二分作为它们之间的桥梁。具体来说二分操作次数 \(k\),如果 \(x<n\) 就说明这个 \(k\) 合法,最后的答案就是 \(k+1\)(网上的题解把二分解释为转化判定问题,我不敢苟同)
考虑这个新问题怎么设计状态,一次操作对应着一段字符串,要满足相邻两个字符串之间不能产生 \(t\) 的子串,充要条件就是前面一个字符串在末尾添加上下一个字符串的第一个字符不能是 \(t\) 的子串。
这说明我们只需要记录开头段的第一个字符,设 \(f_{i,c}\) 表示 \(i\) 次操作,开头段的第一个字符是 \(c\),不能构造出来的字符串最短是 \(f_{i,c}+1\),设 \(g_{c,d}\) 表示以字符 \(c\) 开头,末尾接上字符 \(d\) 之后就不再是子串 的最短子串。那么转移拼一段上去:
那么可以直接用矩阵乘法优化,二分的过程可以用倍增实现,时间复杂度 \(O(t|\Sigma|^2+\log n|\Sigma|^3)\)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define int long long
const int M = 200005;
const int inf = 1e18;
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,last,ans,d[M][4],vis[M];char s[M];
struct node{int fa,len,ch[4];}a[M];
struct mat
{
int a[4][4];
mat() {memset(a,0x3f,sizeof a);}
mat operator * (const mat &b) const
{
mat r;
for(int i=0;i<4;i++) for(int j=0;j<4;j++)
for(int k=0;k<4;k++)
r.a[i][k]=min(r.a[i][k],a[i][j]+b.a[j][k]);
return r;
}
}A[65],r;
void add(int c)
{
int p=last,np=last=++cnt;
a[np].len=a[p].len+1;
for(;!a[p].ch[c] && p;p=a[p].fa) a[p].ch[c]=np;
if(!p) a[np].fa=1;
else
{
int q=a[p].ch[c];
if(a[q].len==a[p].len+1) a[np].fa=q;
else
{
int nq=++cnt;a[nq]=a[q];
a[nq].len=a[p].len+1;
a[q].fa=a[np].fa=nq;
for(;a[p].ch[c]==q;p=a[p].fa)
a[p].ch[c]=nq;
}
}
}
void dfs(int u)
{
if(vis[u]) return ;vis[u]=1;
for(int i=0;i<4;i++) dfs(a[u].ch[i]);
for(int i=0;i<4;i++)
{
d[u][i]=a[u].ch[i]?inf:0;
for(int j=0;j<4;j++) if(a[u].ch[j])
d[u][i]=min(d[u][i],d[a[u].ch[j]][i]+1);
}
}
signed main()
{
n=read();scanf("%s",s+1);
m=strlen(s+1);cnt=last=1;
for(int i=1;i<=m;i++) add(s[i]-'A');
dfs(1);
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
A[0].a[i][j]=d[a[1].ch[i]][j]+1;
for(int i=1;i<=60;i++) A[i]=A[i-1]*A[i-1];
for(int i=0;i<4;i++) r.a[i][i]=0;
for(int i=60;i>=0;i--)
{
mat tmp=r*A[i];int len=inf;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
len=min(len,tmp.a[i][j]);
if(len<n)
ans+=1ll<<i,r=tmp;
}
printf("%lld\n",ans+1);
}
The Winds of Winter
题目描述
题目读了好久,建议一定要手玩样例确认自己理解对没有。
解法
考虑对于固定的点 \(v\) 如何计算答案,我们先以 \(v\) 建树,那么要操作的点 \(x\) 一定落在 \(v\) 的重子树中,设 最大子树 \(/\) 次大子树 \(/\) 最小子树的大小分别为 \(mx\ /\ cx\ /\ mn\),设操作的子树大小为 \(s\),那么满足答案为:
最小化答案可以考虑二分,即找到最小的 \(ans\) 同时满足 \(ans\geq mx-s,ans\geq cx,ans\geq mn+s\),如果我们可以维护出重儿子内子树大小的所有可能,那么我们就可以解决这道题。
考虑重儿子可能是 \(v\) 在原树的重儿子,或者是 \(v\) 在原树上的父亲。考虑前者怎么维护都可以,父亲方向我们可以维护 到根链上节点的所有子树 \(A\)(会受到当前子树大小的影响)和 非到根链上的所有子树 \(B\)(不受到当前子树大小的影响)
可以在 \(\tt dfs\) 的过程中动态地维护这两者,维护 \(A\) 是容易的,维护 \(B\) 需要在递归进入时删除,在回溯时又添加回去,直接做复杂度爆炸。但是考虑这种子树暴力添加删除的问题可以用 \(\tt dsu\) 优化复杂度。
具体来说我们先访问轻儿子,结束轻儿子的递归时再添加回来,再访问重儿子,再把轻儿子的子树删除,这样 \(B\) 就是不含 \(v\) 的子树的,可以直接用于询问,并且我们一直都只对轻儿子操作所以复杂度是对的。这样在原树的重儿子也可以轻松地维护,使用 multiset
,时间复杂度 \(O(n\log ^2n)\)
现在想想本题直接用线段树合并暴力维护也不是不行,但是实现起来可能就要难很多了。
#include <cstdio>
#include <vector>
#include <iostream>
#include <set>
using namespace std;
const int M = 100005;
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,rt,fa[M],son[M],siz[M],ans[M];vector<int> g[M];
multiset<int> A,B,s[M];int mx[M],mn[M],cx[M];
void pre(int u,int p)
{
siz[u]=1;fa[u]=p;
for(int v:g[u]) if(v^p)
{
pre(v,u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]) son[u]=v;
}
if(u!=rt) B.insert(siz[u]);
}
int check(int u,int c)
{
if(siz[son[u]]==mx[u])
{
auto it=s[u].lower_bound(mx[u]-c);
return it!=s[u].end() && *it<=c-mn[u];
}
auto it=B.lower_bound(mx[u]-c);
if(it!=B.end() && *it<=c-mn[u]) return 1;
it=A.lower_bound(mx[u]-c+siz[u]);
return it!=A.end() && *it<=c-mn[u]+siz[u];
}
void dfs(int u)
{
if(u!=rt)
{
B.erase(B.find(siz[u]));
if(fa[u]!=rt) A.insert(siz[fa[u]]);
}
for(int v:g[u]) if(v^fa[u] && v^son[u])
{
dfs(v);
for(int x:s[v]) B.insert(x);
}
mx[u]=max(n-siz[u],siz[son[u]]);
cx[u]=min(n-siz[u],siz[son[u]]);
mn[u]=(siz[u]==n)?siz[son[u]]:cx[u];
if(son[u])
{
dfs(son[u]);
swap(s[u],s[son[u]]);
}
for(int v:g[u]) if(v^fa[u] && v^son[u])
{
cx[u]=max(cx[u],siz[v]);
mn[u]=min(mn[u],siz[v]);
for(int x:s[v]) B.erase(B.find(x));
}
int l=cx[u],r=mx[u];ans[u]=mx[u];
if(mn[u]) while(l<=r)
{
int mid=(l+r)>>1;
if(check(u,mid)) ans[u]=mid,r=mid-1;
else l=mid+1;
}
for(int v:g[u]) if(v^fa[u] && v^son[u])
for(int x:s[v]) s[u].insert(x);
if(u!=rt && fa[u]!=rt) A.erase(A.find(siz[fa[u]]));
s[u].insert(siz[u]);
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
int u=read(),v=read();
if(!u || !v) rt=u+v;
else g[u].push_back(v),g[v].push_back(u);
}
pre(rt,0);
dfs(rt);
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
}