HNOI 2018 题目选做
前言
受到 \(\tt werner\_yin\) 鸽鸽的启发,我要开始刷 \(\tt HNOI\) 了。
还是每天更至少三题的博客,\(\tt zxy\) 绝不断更。
结果一天真的就只更了三道题呗,我效率是真的低啊...刷题还是不能这么慢啊...
2018 排列
题目描述
解法
题目描述特别混乱,观察样例解释可以把问题转化成:连一条 \(a_i\rightarrow i\) 的有向边,要求如果 \((i,j)\) 有边那么需要满足 \(p_i<p_j\),试确定 \(p\) 使得 \(\prod_{i=1}^n p_i\cdot w_i\) 最大。
首先如果构成环那么答案一定是 \(-1\),否则图的结构一定是以 \(0\) 为根的一棵有根树。我们可以依次从 \(1\rightarrow n\) 在树上选取对应的点,要求选某一个点之前必须选它的父亲。
到这里其实我们把问题转化成了一个经典模型:牛半仙的魔塔,也就是我们考虑无限制时会选取哪个点最优,但是实际上我们需要先选取它的父亲才能选他,这说明选取它的父亲之后一定会选他,这构成了一个很强的限制关系,所以我们在修正答案之后把它和父亲绑定即可。
那么我们如何判断到底哪个"点"(实际上是一个绑定好顺序的序列)才是最优的呢?设两个序列 \(A,B\) 的长度分别是 \(m_1,m_2\),那么考虑 \(A\) 放在前面的条件是:
也就是平均值小的点需要先行选取,绑定的时候直接更改 \(w\) 之和与序列长度,再把修改过后的"点"塞进优先队列即可,新增的贡献是 \(m_{fa}\cdot w_u\),时间复杂度 \(O(n\log n)\)
总结
树上的父亲-儿子
类限制可以思考绑定类贪心模型。
#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
#define int long long
const int M = 500005;
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,cnt,ans,a[M],siz[M],vis[M],fa[M],par[M];
vector<int> g[M];
struct node
{
int rt,x,y;
bool operator < (const node &r) const
{
return x*r.y>r.x*y;
}
};priority_queue<node> q;
void dfs(int u,int fa)
{
vis[u]=1;cnt++;par[u]=fa;
for(int v:g[u]) if(!vis[v]) dfs(v,u);
}
int find(int x)
{
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
int j=read();
g[j].push_back(i);
}
dfs(0,0);
if(cnt<=n) {puts("-1");return 0;}
for(int i=0;i<=n;i++) siz[i]=1,fa[i]=i;
for(int i=1;i<=n;i++)
a[i]=read(),q.push(node{i,a[i],1});
while(!q.empty())
{
node t=q.top();int u=t.rt;q.pop();
if(siz[u]!=t.y) continue;
int p=fa[u]=find(par[u]);
ans+=a[u]*siz[p];a[p]+=a[u];siz[p]+=siz[u];
if(p) q.push(node{p,a[p],siz[p]});c++
}
printf("%lld\n",ans);
}
2018 游戏
题目描述
解法
不难发现每个点能到达的范围是 \([l_i,r_i]\) 的形式,针对这类问题我们需要有继承的思想,也就是如果你走到点 \(k\),那么你走到 \([l_k,r_k]\) 这个范围就是充分的,所以你可以继承点 \(k\) 的信息来加速计算。
首先考虑 \(y\leq x\) 的简单情况,也就是所有钥匙一定在房间的左边,那么很显然我们只能向右通过上锁的门。设 \(a_x\) 表示门 \((x,x+1)\) 的钥匙放的位置,那么门能通过的条件是 \(a_x\geq l_i\)(注意此时 \(l_i\) 是一开始就知道的),我们从右到左扫描,用一个单调栈来维护作为瓶颈的门,根据继承的思想当一个门不再成为瓶颈的时候是可以直接弹出的,最后我们取栈顶继承。
回到本题,复杂的地方是 \(l_i\) 是随着 \(r_i\) 变化的,那么我们只需要在 \(r_i\) 变化时快速计算出 \(l_i\) 即可。考虑左边钥匙在更左边的门一定是过不去的,我们可以预处理出左端点的大致范围 \(l_i\geq ls_i\),那么剩下的门只用考虑钥匙在右边的情况。此时能走过的条件是:\(a_x\leq r_i\),我们用线段树处理出 \([ls_i,prel_i)\) 之间的第一个大于 \(r_i\) 的位置,那么时间复杂度 \(O(n\log n)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1000005;
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,q,s[M],a[M],l[M],r[M],ls[M],tr[M<<2];
void build(int i,int l,int r)
{
if(l==r) {tr[i]=a[l];return ;}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
tr[i]=max(tr[i<<1],tr[i<<1|1]);
}
int find(int i,int l,int r,int x)
{
if(tr[i]<=x) return 0;
if(l==r) return l;
int mid=(l+r)>>1;
return tr[i<<1|1]>x?find(i<<1|1,mid+1,r,x):
find(i<<1,l,mid,x);
}
int ask(int i,int l,int r,int L,int R,int x)
{
if(L>r || l>R) return 0;
if(L<=l && r<=R) return find(i,l,r,x);
int mid=(l+r)>>1,p=ask(i<<1|1,mid+1,r,L,R,x);
if(p) return p;
p=ask(i<<1,l,mid,L,R,x);
return p;
}
int get(int l,int r,int x)
{
if(l>r) return l;
int p=ask(1,1,n,l,r,x);
return p?p+1:l;
}
void init()
{
build(1,1,n);a[n]=n+1;a[0]=-1;
for(int i=1;i<=n;i++)
ls[i]=a[i-1]&&a[i-1]<=i-1?i:ls[i-1];
for(int i=n,t=0;i>=1;i--)
{
l[i]=r[i]=i;
l[i]=get(ls[i],l[i]-1,r[i]);
s[++t]=i;
while(t && (!a[s[t]] || l[i]<=a[s[t]] && a[s[t]]<=r[i]))
{
r[i]=s[--t];
l[i]=get(ls[i],l[i]-1,r[i]);
}
}
}
signed main()
{
n=read();m=read();q=read();
for(int i=1,x,y;i<=m;i++)
x=read(),y=read(),a[x]=y;
init();
while(q--)
{
int x=read(),y=read();
puts(l[x]<=y && y<=r[x]?"YES":"NO");
}
}
2018 毒瘤
题目描述
解法
其实这题掌握了简洁的写法就不那么毒瘤了,但是我确实做不出来,这些题一道都做不出来。
独立集计数这个问题肯定是不可做的,但是树上独立集计数却可以做到 \(O(n)\),这提示我们可以暴力枚举非树边的状态然后暴力做树 \(dp\) 即可,具体来说我们枚举浅的那个点是否选取,然后在 \(dp\) 的过程中可以排除掉一些不合法的情况,时间复杂度 \(O(2^{m-n+1}\cdot n)\),这么傻的做法竟然有 \(75\) 分!
慢的原因是我们做了很多重复计算,考虑很多点的 \(dp\) 值其实是不受枚举影响的,很多点的 \(dp\) 值受影响的程度很小。发现这东西很像虚树(请原谅我苍白的引出),所以我们建出所有非树边连接点的虚树,但是计数类问题的虚树和最值类问题的虚树是有区别的,不在虚树上的点也会有很大的影响,先观察一下原来的转移方程吧:
我们可以在脑海中使用一下分配律,设点 \(u\) 子树内离他最近的虚树点是 \(x\)(可以是自己),那么 \(dp\) 值可以表示成:
\(k[u][0/1][0/1]\) 表示一个系数数组,它是不受枚举影响的,我们考虑依照定义计算出这个数组,需要分类讨论,此外我们需要计算 \(p[u][0/1]\) 表示不考虑含有虚点的子树计算出来的 \(dp\) 值:
- 如果这个点就是虚树上的点,那么 \(k[u][0][0]=1,k[u][1][1]=1\)(也就是 \(u=x\))
- 如果这个点不是虚树上的点,包含虚点的子树最多只有一个,如果有的话那么 \(k[u][0]=k[v][0]+k[v][1]\),\(k[u][1]=k[v][0]\),加法定义成 \(\tt pair\) 的对应位相加,最后再用分别乘上 \(p[u][0/1]\) 即可。
设 \((u,v)\) 在原树上有边,那么我们可以根据 \(k\) 计算 \(v\) 和 \(x_v\) 的 \(dp\) 值关系式,建立虚树的时候就可以直接把 \(u\) 和 \(x_v\) 连边,然后把 \(x_v\) 的 \(dp\) 值转移上来,设 \(s=m-n+1\),时间复杂度 \(O(n+s\cdot 2^s)\)
总结
计数类虚树可以计算转移的系数(相当于把链压缩起来)然后在虚树上转移。
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 998244353;
#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,t,cnt,ans,ea[M],eb[M],dfn[M],mark[M],sz[M];
vector<int> g[M],ng[M];int f[M][2],fl[M][2],p[M][2];
struct node
{
int x,y;node(int X=0,int Y=0) : x(X) , y(Y) {}
node operator + (const node &b) const
{return node((x+b.x)%MOD,(y+b.y)%MOD);}
node operator * (const int &b) const
{return node(x*b%MOD,y*b%MOD);}
}k[M][2],o[M][2];
void dfs(int u,int fa)
{
dfn[u]=++cnt;
for(int v:g[u]) if(v!=fa)
{
if(!dfn[v]) dfs(v,u),sz[u]+=sz[v];
else
{
mark[u]=1;
if(dfn[u]<dfn[v]) ea[++t]=u,eb[t]=v;
}
}
mark[u]|=sz[u]>=2;sz[u]=sz[u]||mark[u];
}
int pre(int u)
{
p[u][0]=p[u][1]=1;dfn[u]=1;int pos=0;
for(int v:g[u]) if(!dfn[v])
{
int w=pre(v);
if(!w)
{
p[u][0]=p[u][0]*(p[v][0]+p[v][1])%MOD;
p[u][1]=p[u][1]*p[v][0]%MOD;
}
else if(mark[u])
{
ng[u].push_back(w);
o[w][0]=k[v][0]+k[v][1];
o[w][1]=k[v][0];
}
else k[u][1]=k[v][0],k[u][0]=k[v][0]+k[v][1],pos=w;
}
if(mark[u]) k[u][0]=node(1,0),k[u][1]=node(0,1),pos=u;
else k[u][0]=k[u][0]*p[u][0],k[u][1]=k[u][1]*p[u][1];
return pos;
}
void calc(int u)
{
f[u][0]=fl[u][0]?0:p[u][0];
f[u][1]=fl[u][1]?0:p[u][1];
for(int v:ng[u])
{
calc(v);int p=f[v][0],q=f[v][1];
f[u][0]=f[u][0]*((p*o[v][0].x+q*o[v][0].y)%MOD)%MOD;
f[u][1]=f[u][1]*((p*o[v][1].x+q*o[v][1].y)%MOD)%MOD;
}
}
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);mark[1]=1;
memset(dfn,0,sizeof dfn);pre(1);
for(int i=0;i<(1<<t);i++)
{
for(int j=1;j<=t;j++)//1:choose ; 0:not choose
if(i&(1<<j-1)) fl[ea[j]][0]=fl[eb[j]][1]=1;c++
else fl[ea[j]][1]=1;
calc(1);
ans=(ans+f[1][0]+f[1][1])%MOD;
for(int j=1;j<=t;j++)
if(i&(1<<j-1)) fl[ea[j]][0]=fl[eb[j]][1]=0;
else fl[ea[j]][1]=0;
}
printf("%lld\n",ans);
}
2018 转盘
题目描述
解法
这道题我搞了差不多一个下午啊,怎么 \(\tt HNOI\) 这么花时间啊,我发现我不打球效率高不起来,然后我就去打球了。
拿到这道题发现暴力都不会打然后就开始自闭,实际上问题在于每个时刻我们都要决策停下还是继续走,但显然我们没有时间拿来给你决策,这时候就可以考虑去推结论,关于最优决策的结论。
现在感觉上最正确的结论是存在最优方案可以一直走不停下,证明可以考虑调整法,也就是考虑一个需要停下的最优方案,我们找到最后一个停下的点 \(x\),然后把起点往前挪一格就可以在点 \(x\) 不停下,我们考虑重要的是最后一次到点 \(i\) 的时间 \(t_i\),那么调整之后所有 \(t_i\) 都是不降的,这说明了我们的调整是合法的。
还有另一种证明方法,考虑我们决策停下的原因是要等待物品出现,我们可以让时光倒流使出现变成消失,我们从结束时间 \(t\) 开始走,每次可以往回走一步或者停下,每个物品在 \(T_i\) 时刻消失,发现如果这样停下是绝对不优的,我们要一直走才能够访问完所有物品。
我们破环成链,在数组的后面接上 \([1,n]\),那么我们可以枚举终止位置 \(i\),设终止的时间为 \(T\),然后我们让时光倒流。我们考虑物品 \(j\) 可以被标记到的条件是:
设 \(a_i=t_i-i\),然后我们顺便调整一下枚举范围让它看起来更美观:
这个结构太复杂了,由于我们只想求出 \(T_{\min}\),我们可以考虑 \(j\) 对答案的贡献,当 \(a_j\) 是后缀最大值的时候才有贡献。然后我们找到对应的最小的 \(i\),发现这就是一个单调栈模型,我们建一个从前往后递减的单调栈,设栈内第 \(i\) 个元素是 \(p_i\):
问题变成了好像没有怎么见过的维护单调栈,但是可以转化成从后往前的极长上升子序列,那么套用楼房重建的方法即可。每次我们在 \(p_{i-1}\) 这个位置把 \(a_{p_i}+p_{i-1}\) 这东西计算进去,还有一个细节是我们可以直接把 \(\max-n\) 当成初值传进计算函数就可以不用考虑后 \(n\) 位了,因为 \((n,2n]\) 里面的最大值是前面的最大值减 \(n\)
时间复杂度 \(O(n\log^2 n)\)
总结
维护单调栈问题可以转化成极长上升子序列问题然后套用楼房重建模型。
当某个式子难以维护的时候,可以尝试切换主体。
遇到需要决策的问题时,常见的结论是某一种操作永远不会用到,普遍的证明方法可以考虑调整法。当然如果遇到了出现问题让时光倒流将其转化成消失问题可能有助于发现结论。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
const int inf = 0x3f3f3f3f;
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,p,a[M],mx[M<<2],tr[M<<2];
int ask(int i,int l,int r,int c)
{
if(l==r) return mx[i]>c?c+l:inf;
int mid=(l+r)>>1;
return mx[i<<1|1]>c?min(tr[i],
ask(i<<1|1,mid+1,r,c)):ask(i<<1,l,mid,c);
}
void ins(int i,int l,int r,int id,int c)
{
if(l==r) {mx[i]=c-l;return ;}
int mid=(l+r)>>1;
if(mid>=id) ins(i<<1,l,mid,id,c);
else ins(i<<1|1,mid+1,r,id,c);
mx[i]=max(mx[i<<1],mx[i<<1|1]);
tr[i]=ask(i<<1,l,mid,mx[i<<1|1]);
}
signed main()
{
n=read();m=read();p=read();
for(int i=1;i<=n;i++)
ins(1,1,n,i,read());
int ans=ask(1,1,n,mx[1]-n)+n;
printf("%d\n",ans);
while(m--)
{
int x=read()^(!p?0:ans),y=read()^(!p?0:ans);
ins(1,1,n,x,y);ans=ask(1,1,n,mx[1]-n)+n;
printf("%d\n",ans);
}
}
2018 寻宝游戏
题目描述
解法
我暂时对本题结论是如何想到的没有任何头绪,可能以后想清楚了会再做补充。
首先对最基本 \(01\) 分析一波性质,考虑 and 1/or 0
对结果是没有任何影响的。而 and 0
会导致删除这个位置上的 \(1\),or 1
会导致添加这个位置上的 \(1\),我们称这两个操作为有效操作。
我们从最后结果的角度来对过程提出一些要求,如果结果是 \(1\),那么最后的有效操作一定是添加;如果结果是 \(0\),那么最后的有效操作一定是删除,或者全局都没有有效操作也是合法的。
我想到上面这部分就想不动了,正解莫名其妙地整出来一个字典序,就是说如果我们把操作序列中的 or
当成 \(0\),and
当成 \(1\),然后从 \(n\rightarrow 1\) 排列,把原序列上第 \(i\) 位的所有数字也从 \(n\rightarrow 1\) 排列,那么结果是 \(1\) 的充要条件是原序列的字典序大于操作序列的字典序,结果是 \(0\) 的充要条件是原序列的字典序小于等于操作序列的字典序。
把结论告诉你之后是非常容易证明的,因为如果字符相等代表的是无效操作,可以跳到下一位继续比较。如果字符不等那么考虑是添加还是删除这两种情况,直接决定了最后是 \(1\) 还是 \(0\),并且区分出了字典序。
我们把 \(m\) 个原序列按字典序大小排序,那么给出串的 \(0/1\) 就代表了操作序列和原序列的偏序关系,那么操作序列的取值一定是夹在两个原序列之间的,那么直接把对应的值相减就能得到方案数。
#include <cstdio>
const int M = 5005;
const int MOD = 1e9+7;
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,q,rk[M],t[M],a[M],b[M][M];char s[M];
signed main()
{
n=read();m=read();q=read();
for(int i=1;i<=m;i++) rk[i]=i;
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);int o=0;
for(int j=1;j<=m;j++) b[j][i]=a[j]=s[j]-'0';
for(int j=1;j<=m;j++) if(a[rk[j]]==0) t[++o]=rk[j];
for(int j=1;j<=m;j++) if(a[rk[j]]==1) t[++o]=rk[j];
for(int j=1;j<=m;j++) rk[j]=t[j];
}
for(int j=1;j<=m;j++) a[j]=0;
for(int j=1;j<=m;j++) for(int i=n;i>=1;i--)
a[j]=(2*a[j]+b[j][i])%MOD;
for(int i=1;i<=n;i++) a[m+1]=(2*a[m+1]+1)%MOD;
a[m+1]++;rk[m+1]=m+1;
while(q--)
{
scanf("%s",s+1);int l=0,r=m+1;
for(int i=1;i<=m;i++) if(s[rk[i]]=='1') {r=i;break;}
for(int i=m;i>=1;i--) if(s[rk[i]]=='0') {l=i;break;}
printf("%d\n",(r<l)?0:(a[rk[r]]-a[rk[l]]+MOD)%MOD);
}
}