HNOI2018简要题解
HNOI2018简要题解
D1T1 寻宝游戏
题意
某大学每年都会有一次 Mystery Hunt 的活动,玩家需要根据设置的线索解谜,找到宝藏的位置,前一年获胜的队伍可以获得这一年出题的机会。
作为新生的你对这个活动非常感兴趣。你每天都要从西向东经过教学楼一条很长的走廊,这条走廊是如此的长,以至于它被人戏称为 infinite corridor。一次,你经过这条走廊的时,注意到在走廊的墙壁上隐藏着 \(n\) 个等长的二进制的数字,长度均为 \(m\)。你从西向东将这些数字记录了下来,形成一个含有 \(n\) 个数的二进制数组 \(a_1, a_2, ..., a_n\)。很快,在最新的一期 Voo Doo 杂志上,你发现了 \(q\) 个长度也为 \(m\) 的二进制串 \(r_1, r_2, ..., r_q\)。聪明的你很快发现了这些数字的含义。保持数组 \(a_1, a_2, ..., a_n\) 的元素顺序不变,你可以在它们之间插入 \(\wedge\)(按位与运算)或者 \(\vee\)(按位或运算)两种二进制运算符。例如:\(11011 \wedge 00111=00011,11011 \vee 00111=11111\)。
你需要插入恰好 \(n\) 个运算符,相邻两个数之间恰好一个,在第一个数的左边还有一个。如果我们在第一个运算符的左边补入一个 \(0\),这就形成了一个运算式,我们可以计算它的值。与往常一样,运算顺序是从左往右。有趣的是,出题人已经告诉你这个值的可能的集合——Voo Doo 杂志里的那一些二进制数 \(r_1, r_2, ..., r_q\),而解谜的方法,就是对 \(r_1, r_2, ..., r_q\) 中的每一个值 \(r_i\),分别计算出有多少种方法填入这 \(n\) 个运算符,使得这个运算式的值是 \(r_i\) 。然而,infinite corridor 真的很长,这意味着数据范围可能非常大。因此,答案也可能非常大,但是你发现由于谜题的特殊性,你只需要求答案模 \(1000000007\)(\(10^9 + 7\),一个质数)的值。
对于 \(10\%\) 的数据,\(n \le 20, m \le 30\),\(q = 1\)
对于另外 \(20\%\) 的数据,\(n \le 1000\),\(m \le 16\)
对于另外 \(40\%\) 的数据,\(n \le 500\),\(m \le 1000\)
对于 \(100\%\) 的数据,\(1 \le n \le 1000\),\(1 \le m \le 5000\),\(1 \le q \le 1000\)
题解
orz myy. 神题。
发现\(|1\)和\(\& 0\)后的结果是一定的,所以某一位最后为1,则要求最后一个&0的位置要在|1之前。
据说这样从后往前爆搜,及时break可以得到70分?!
然后考虑把操作序列量化成01串,&=1,|=0,则对于某一位来说,从后往前,当操作串的字典序小于运算元素的串,则最后运算结果为1。
这样就很好处理了,把这m个串抠出来,操作串要小于其中一些字串的字典序,大于等于另一些的。也就是\(x\le op<y\),把\(x,y\)转成二进制数后算差就是op的数量了。对这些串排序后算相邻两数差,最后要么没有答案要么是某相邻两数之差。
注意考场没有开O2所以最好用Trie树排序或者鸡排。
复杂度\(\mathcal O(nm)\)。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<vector>
#define pb push_back
using namespace std;
const int N=1100,M=5100,Tr=5e6+10,mod=1e9+7;
int n,m,q,nod=1,ch[Tr][2],rk[M],tt,bit[N],d[M];
char s[N][M];
vector<int> tag[Tr];
struct Num
{
int s[N],rk;
int operator - (Num A) const
{
int res=0;
for(int i=n;i>=1;i--)
if(s[i]!=A.s[i])
(res+=1ll*(s[i]-A.s[i]+mod)*bit[n-i+1]%mod)%=mod;
return res;
}
}A[M];
void Insert(Num A,int id)
{
int x=1;
for(int i=1;i<=n;i++)
{
int &v=ch[x][A.s[i]];
if(!v) v=++nod;x=v;
}
tag[x].pb(id);
}
void dfs(int x)
{
for(int l=tag[x].size(),i=0;i<l;i++)
rk[++tt]=tag[x][i];
if(ch[x][0]) dfs(ch[x][0]);
if(ch[x][1]) dfs(ch[x][1]);
}
int main()
{
cin>>n>>m>>q;bit[1]=1;
for(int i=2;i<=n;i++) bit[i]=2ll*bit[i-1]%mod;
for(int i=1;i<=n;i++) scanf("%s",s[i]+1);
for(int i=1;i<=m;i++)
for(int j=n,p=0;j>=1;j--)
A[i].s[++p]=s[j][i]-'0';
for(int i=1;i<=m;i++) Insert(A[i],i);
dfs(1);
for(int i=1;i<=m;i++) A[rk[i]].rk=i;
for(int i=1;i<m;i++) d[i]=A[rk[i+1]]-A[rk[i]];
for(int i=1;i<=n;i++) A[m+1].s[i]=0,A[m+2].s[i]=1;
d[0]=A[rk[1]]-A[m+1],d[m]=(A[m+2]-A[rk[m]]+1)%mod;
for(int w=1;w<=q;w++)
{
scanf("%s",s[0]+1);
int ans=0,mxl=0,mnr=m+1;
for(int i=1;i<=m;i++)
if(s[0][i]=='1') mnr=min(mnr,A[i].rk);
else mxl=max(mxl,A[i].rk);
if(mxl<mnr) ans=d[mxl];
printf("%d\n",ans);
}
return 0;
}
D1T2 转盘
题意
一次小 G 和小 H 原本准备去聚餐,但由于太麻烦了于是题面简化如下:
一个转盘上有摆成一圈的 \(n\) 个物品(编号 \(1\) 至 \(n\))其中第 \(i\) 个物品会在 \(T_i\) 时刻出现。
在 \(0\) 时刻时,小 G 可以任选 \(n\) 个物品中的一个,我们将其编号记为 \(s_0\)。并且如果 \(i\) 时刻选择了物品 \(s_i\),那么 \(i + 1\) 时刻可以继续选择当前物品或者选择下一个物品。当 \(s_i\) 为 \(n\) 时,下一个物品为物品 \(1\),否则下一个物品为 \(s_{i} + 1\)。在每一时刻(包括 \(0\) 时刻),如果小 G 所选择的物品已经出现了,那么小 G 将会标记它。小 H 想知道,在物品选择的最优策略下,小 G 什么时候能标记所有物品?
但麻烦的是,物品的出现时间会不时修改。我们将其描述为 \(m\) 次修改,每次修改将改变其中一个物品的出现时间。每次修改之后,你也需要求出当前局面的答案。对于其中部分测试点,小 H 还追加了强制在线的要求。
| 测试点编号 | \(n\) | \(m\) | \(T_i/T_x\) | \(p\) |
|---|---|---|---|---|
| 1 | \(\le 10\) | \(\le 10\) | \(\le 10\) | \(=0\) |
| 2 | \(\le 1000\) | \(=0\) | \(\le 1000\) | \(=0\) |
| 3 | \(\le 10^5\) | \(=0\) | \(\le 10^5\) | \(=0\) |
| 4 | \(\le 5000\) | \(\le 5000\) | \(\le 10^5\) | \(=0\) |
| 5 | \(\le 8\times 10^4\) | \(\le 8\times 10^4\) | \(\le 10^5\) | \(=0\) |
| 6 | \(\le 8\times 10^4\) | \(\le 8\times 10^4\) | \(\le 10^5\) | \(=1\) |
| 7 | \(\le 9\times 10^4\) | \(\le 9\times 10^4\) | \(\le 10^5\) | \(=0\) |
| 8 | \(\le 9\times 10^4\) | \(\le 9\times 10^4\) | \(\le 10^5\) | \(=1\) |
| 9 | \(\le 10^5\) | \(\le 10^5\) | \(\le 10^5\) | \(=0\) |
| 10 | \(\le 10^5\) | \(\le 10^5\) | \(\le 10^5\) | \(=1\) |
题解
我真佩服去年的自己、竟然有40分,而今天看了好久才看懂去年的做法。
首先可以证明的是一定是只走一圈。
去年的40分做法:
序列倍长后,\(a[i]=T[i]-i\),对于a维护单调递减队列,答案为n个滑动窗口的队头+i。可以把a看成是等待时间,最后加上n-1就是真正的答案了。
AC做法:
其实答案求的就是$$min_{i=1}{n}[max_{j=i}{i+n}A_j+i]$$。
发现\(A_j>A_{j+n}\)后,式子里的max就可以换成后缀max了。考虑用线段树维护这个东西。
每个节点\((l,r)\)维护\(mx[x]\)表示最大的A,\(ans[x]\)表示\(i\)取到\([l,mid]\)时候的最小答案。
合并信息就重新递归一下,和男神那题超级像。
复杂度\(\mathcal O(nlog^2n)\)。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
const int N=2e5+10;
int n,m,op,T[N],a[N],ans[N<<2],mx[N<<2],Ans;
int calc(int x,int l,int r,int b)
{
if(l==r) return l+max(mx[x],b);
int mid=(l+r)>>1;
if(mx[x<<1|1]>=b) return min(ans[x],calc(x<<1|1,mid+1,r,b));
else return min(calc(x<<1,l,mid,b),mid+1+b);
}
void pushup(int x,int l,int r)
{
mx[x]=max(mx[x<<1],mx[x<<1|1]);
ans[x]=calc(x<<1,l,(l+r)>>1,mx[x<<1|1]);
}
void build(int x,int l,int r)
{
if(l==r) {mx[x]=a[l],ans[x]=T[l];return;}
int mid=(l+r)>>1;
build(x<<1,l,mid);
build(x<<1|1,mid+1,r);
pushup(x,l,r);
}
void update(int x,int l,int r,int p)
{
if(l==r) {mx[x]=a[l],ans[x]=T[l];return;}
int mid=(l+r)>>1;
if(p<=mid) update(x<<1,l,mid,p);
else update(x<<1|1,mid+1,r,p);
pushup(x,l,r);
}
int main()
{
cin>>n>>m>>op;
for(int i=1;i<=n;i++)
{
scanf("%d",&T[i]);a[i]=T[i]-i;
T[i+n]=T[i],a[i+n]=T[i]-(i+n);
}
build(1,1,n*2);printf("%d\n",Ans=ans[1]+n-1);
for(int i=1,x,y;i<=m;i++)
{
scanf("%d%d",&x,&y);
if(op) x^=Ans,y^=Ans;
T[x]=T[x+n]=y,a[x]=y-x,a[x+n]=y-x-n;
update(1,1,n*2,x),update(1,1,n*2,x+n);
printf("%d\n",Ans=ans[1]+n-1);
}
return 0;
}
D1T3 毒瘤
题意
从前有一名毒瘤。
毒瘤最近发现了量产毒瘤题的奥秘。考虑如下类型的数据结构题:给出一个数组,要求支持若干种奇奇怪怪的修改操作(例如给一个区间内的数同时加上 \(c\),或者将一个区间内的数同时开平方根),并且支持询问区间的和。毒瘤考虑了 \(n\) 个这样的修改操作,并将它们编号为 \(1 \ldots n\)。当毒瘤要出数据结构题的时候,他就将这些修改操作中选若干个出来,然后出成一道题。
当然了,这样出的题有可能不可做。通过精妙的数学推理,毒瘤揭露了这些修改操作之间的关系:有 \(m\) 对「互相排斥」的修改操作,第 \(i\) 对是第 \(u_i\) 个操作和第 \(v_i\) 个操作。当一道题中同时含有 \(u_i\) 和 \(v_i\) 这两个操作时,这道题就会变得不可做。另一方面,当一道题中不包含任何「互相排斥」的操作时,这个题就是可做的。此外,毒瘤还发现了一个规律:\(m − n\) 是一个很小的数字(参见「数据范围」中的说明),且任意两个修改操作都是连通的。两个修改操作 \(a, b\) 是连通的,当且仅当存在若干操作 \(t_0, t_1, ... , t_l\),使得 \(t_0 = a,t_l = b\),且对任意 \(1 \le i \le l\),\(t_{i−1}\) 和 \(t_i\) 都是「互相排斥」的修改操作。
一对「互相排斥」的修改操作称为互斥对。现在毒瘤想知道,给定值 \(n\) 和 \(m\) 个互斥对,他一共能出出多少道可做的不同的数据结构题。两个数据结构题是不同的,当且仅当其中某个操作出现在了其中一个题中,但是没有出现在另一个题中。
| 测试点 # | 1~4 | 5~6 | 7~8 | 9 | 10~11 | 12~14 | 15~16 | 17~20 |
|---|---|---|---|---|---|---|---|---|
| \(n \le\) | \(20\) | \(10^5\) | \(10^5\) | \(3000\) | \(10^5\) | \(3000\) | \(10^5\) | \(10^5\) |
| \(m \le\) | \(n + 10\) | \(n - 1\) | \(n\) | \(n + 1\) | \(n + 1\) | \(n + 10\) | \(n + 7\) | \(n + 10\) |
题解
就是求有11条返祖边的树的独立集个数。
很良心地给了75左右的暴力容斥部分分。
正解:
把11*2个点抠出来建虚树,一共不到50个点。预处理出没有返祖边的树的dp值。
现在考虑仍然暴力容斥,但是计算过程可以只用在虚树上计算,也就是说优化掉一个\(n\)。
发现虚树上每条边的转移系数是一定的,把未知数代进去转移、就可以预处理出转移系数了。
具体来说我的\(g[x][0/1]=(a,b)\),\(x\)的虚树父亲为\(f\),则
\]
\]
所以显然初值\(g[x][0]=(1,1),g[x][1]=(1,0)\) 。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<vector>
#define pa pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
const int N=2e5+10,mod=998244353;
struct edge{int next,to;}a[N];
int head[N],cnt,n,m,sf[N],fa[N],f[N][2],ST[N][18];
int dfn[N],S[N],c,tot,dep[N],sta[N],top,out[N];
void link(int x,int y) {a[++cnt]=(edge){head[x],y};head[x]=cnt;}
int find(int x) {return sf[x]==x?x:sf[x]=find(sf[x]);}
int ksm(int x,int k)
{
int s=1;for(;k;k>>=1,x=1ll*x*x%mod)
if(k&1) s=1ll*s*x%mod;return s;
}
void dfs(int x,int fr)
{
fa[x]=fr;ST[x][0]=fr;
dep[x]=dep[fr]+1;dfn[x]=++tot;
for(int p=1;p<=16;p++)
ST[x][p]=ST[ST[x][p-1]][p-1];
f[x][0]=f[x][1]=1;
for(int i=head[x];i;i=a[i].next)
{
int R=a[i].to;if(R==fr) continue;
dfs(R,x);
f[x][0]=1ll*f[x][0]*(f[R][0]+f[R][1])%mod;
f[x][1]=1ll*f[x][1]*f[R][0]%mod;
}
out[x]=tot;
}
int LCA(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int p=16;p>=0;p--)
if(dep[ST[x][p]]>=dep[y]) x=ST[x][p];
for(int p=16;p>=0;p--)
if(ST[x][p]!=ST[y][p])
x=ST[x][p],y=ST[y][p];
return x==y?x:ST[x][0];
}
void Jump(int &R,int x)
{
for(int p=16;p>=0;p--)
if(dep[ST[R][p]]>dep[x]) R=ST[R][p];
}
int cmp(int a,int b) {return dfn[a]<dfn[b];}
vector<int> E[N];
int ban[N],g[N][2],s,Ans;
pa f0[N],f1[N],M[N];
void Calc(int x,int y)
{
int p=x;
f0[x]=mp(1,1);f1[x]=mp(1,0);
while(fa[p]!=y)
{
int bs0=1,bs1=1;
for(int i=head[fa[p]];i;i=a[i].next)
{
int R=a[i].to;
if(R==fa[fa[p]]||R==p) continue;
bs0=1ll*bs0*(f[R][0]+f[R][1])%mod;
bs1=1ll*bs1*f[R][0]%mod;
}
pa ff0=mp(1ll*f0[x].fi*bs0%mod,1ll*f0[x].se*bs0%mod);
pa ff1=mp(1ll*f1[x].fi*bs1%mod,1ll*f1[x].se*bs1%mod);
f0[x]=mp((ff0.fi+ff1.fi)%mod,(ff0.se+ff1.se)%mod);
f1[x]=mp(ff0.fi,ff0.se);
p=fa[p];
}
}
int DP()
{
for(int i=1;i<=c;i++) g[S[i]][1]=1,g[S[i]][0]=ban[S[i]]?0:1;
for(int i=c;i>=1;i--)
{
int x=S[i];
g[x][0]*=f[x][0],g[x][1]*=f[x][1];
for(int j=0,l=E[x].size();j<l;j++)
{
int R=E[x][j];Jump(R,x);
g[x][0]=1ll*g[x][0]*ksm((f[R][0]+f[R][1])%mod,mod-2)%mod;
g[x][1]=1ll*g[x][1]*ksm(f[R][0],mod-2)%mod;
}
for(int j=0,l=E[x].size();j<l;j++)
{
int R=E[x][j];
g[x][0]=1ll*g[x][0]*(1ll*f0[R].fi*g[R][0]%mod+1ll*f0[R].se*g[R][1]%mod)%mod;
g[x][1]=1ll*g[x][1]*(1ll*f1[R].fi*g[R][0]%mod+1ll*f1[R].se*g[R][1]%mod)%mod;
}
}
return (g[1][0]+g[1][1])%mod;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) sf[i]=i;
for(int i=1,x,y;i<=m;i++)
{
scanf("%d%d",&x,&y);
if(find(x)!=find(y)) sf[find(x)]=find(y),link(x,y),link(y,x);
else M[++s]=mp(x,y),S[++c]=x,S[++c]=y;
}
dfs(1,0);
sort(S+1,S+c+1,cmp);
for(int i=2,t=c;i<=t;i++) S[++c]=LCA(S[i-1],S[i]);
S[++c]=1;sort(S+1,S+c+1,cmp);
c=unique(S+1,S+c+1)-S-1;
for(int i=1;i<=c;sta[++top]=S[i],i++)
{
while(top&&dfn[S[i]]>out[sta[top]]) top--;
if(top) E[sta[top]].pb(S[i]),Calc(S[i],sta[top]);
}
for(int zt=0,d=1;zt<1<<s;zt++)
{
for(int i=1;i<=s;i++)
if(zt&(1<<(i-1)))
d=mod-d,ban[M[i].fi]=ban[M[i].se]=1;
int res=DP();
(Ans+=1ll*res*d%mod)%=mod;
d=1;for(int i=1;i<=s;i++)
ban[M[i].fi]=ban[M[i].se]=0;
}
cout<<Ans<<endl;
}
D2T1 游戏
题意
一次小 G 和小 H 在玩寻宝游戏,有 \(n\) 个房间排成一列,编号为 \(1,2,…,n\),相邻房间之间都有 \(1\) 道门。其中一部分门上有锁(因此需要对应的钥匙才能开门),其余的门都能直接打开。
现在小 G 告诉了小 H 每把锁的钥匙在哪个房间里(每把锁有且只有一把钥匙),并作出 \(p\) 次指示:第 \(i\) 次让小 H 从第 \(S_i\) 个房间出发,去第 \(T_i\) 个房间寻宝。但是小 G 有时会故意在指令里放入死路,而小 H 也不想浪费多余的体力去尝试,于是想事先调查清楚每次的指令是否存在一条通路。
你是否能为小 H 作出解答呢?
| 测试点编号 | n | m | 其他特性 |
|---|---|---|---|
| 1 | $ \le 1000 $ | $ \le 1000 $ | 无 |
| 2 | $ \le 1000 $ | $ \le 1000 $ | 无 |
| 3 | $ \le 10^5 $ | $ \le 10^5 $ | \(y \le x\) 恒成立 |
| 4 | $ \le 10^5 $ | $ \le 10^5 $ | \(y \le x\) 恒成立 |
| 5 | $ \le 10^5 $ | $ \le 10^5 $ | 无 |
| 6 | $ \le 10^5 $ | $ \le 10^5 $ | 无 |
| 7 | $ \le 10^6 $ | $ \le 10^6 $ | \(y \le x\) 恒成立 |
| 8 | $ \le 10^6 $ | $ \le 10^6 $ | \(y \le x\) 恒成立 |
| 9 | $ \le 10^6 $ | $ \le 10^6 $ | 无 |
| 10 | $ \le 10^6 $ | $ \le 10^6 $ | 无 |
对于所有数据,保证 \(1 \le n,p \le 10^6\),\(0 \le m < n\),\(1 \le x, y, S_i,T_i < n\),保证 \(x\) 不重复。
由于本题输入文件较大,建议在程序中使用读入优化。
题解
被暴力艹过90分真的无语。省选如果出现这种情况退役那也没有什么办法了。
方法是对于每个点维护向左以及向右最多能到的区间。
先用单调栈维护最大可能区间,之后由\([l,r]\)扩展,找到\([LS,l-1]\)中从右往左第一个\(key[i]>r\)的地方,并把\(l\)设置为\(i+1\)。之后便可以拓展右区间。
由于右区间的扩展类似于单调栈,所以不难发现拓展次数最多为\(\mathcal O(n)\)。因此总复杂度为\(\mathcal O(nlogn)\) 。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
const int N=1e6+10;
int n,m,q,key[N],l[N],r[N],t[N<<2];
int sta[N],top,LS[N],RS[N];
void build(int x,int l,int r)
{
if(l==r) {t[x]=key[l];return;}
int mid=(l+r)>>1;
build(x<<1,l,mid);
build(x<<1|1,mid+1,r);
t[x]=max(t[x<<1],t[x<<1|1]);
}
int query(int x,int l,int r,int gl,int gr,int bs)
{
int mid=(l+r)>>1,res=0;
if(l>=gl&&r<=gr)
{
if(t[x]<=bs) return 0;
if(l==r) return l;
if(t[x<<1|1]>bs) return query(x<<1|1,mid+1,r,gl,gr,bs);
else return query(x<<1,l,mid,gl,gr,bs);
}
if(gr>mid) res=query(x<<1|1,mid+1,r,gl,gr,bs);
if(gl<=mid&&!res) res=query(x<<1,l,mid,gl,gr,bs);
return res;
}
int Find(int l,int r,int x)
{
if(l>r) return l;
int p=query(1,1,n,l,r,x);
return p?p+1:l;
}
int main()
{
cin>>n>>m>>q;
for(int i=1,x,y;i<=m;i++) scanf("%d%d",&x,&y),key[x]=y;
build(1,1,n);LS[1]=1,RS[n]=n;
for(int i=2;i<=n;i++) LS[i]=(key[i-1]&&key[i-1]<=i-1)?i:LS[i-1];
for(int i=n-1;i>=1;i--) RS[i]=(key[i]&&key[i]>i)?i:RS[i+1];
for(int i=n;i>=1;i--)
{
r[i]=i;l[i]=Find(LS[i],i-1,i);
while(r[i]<n&&(!key[r[i]]||(key[r[i]]>=l[i]&&key[r[i]]<=r[i])))
r[i]=r[r[i]+1],l[i]=Find(LS[i],l[i]-1,r[i]);
}
for(int i=1,x,y;i<=q;i++)
scanf("%d%d",&x,&y),puts((l[x]<=y&&r[x]>=y)?"YES":"NO");
}
D2T2 排列
题意
给定 \(n\) 个整数 \(a_1, a_2, \ldots , a_n(0 \le a_i \le n)\),以及 \(n\) 个整数 \(w_1, w_2, …, w_n\)。称 \(a_1, a_2, \ldots , a_n\) 的一个排列 \(a_{p[1]}, a_{p[2]}, \ldots , a_{p[n]}\) 为 \(a_1, a_2, \ldots , a_n\) 的一个合法排列,当且仅当该排列满足:对于任意的 \(k\) 和任意的 \(j\),如果 \(j \le k\),那么 \(a_{p[j]}\) 不等于 \(p[k]\)。(换句话说就是:对于任意的 \(k\) 和任意的 \(j\),如果 \(p[k]\) 等于 \(a_{p[j]}\),那么 \(k<j\)。)
定义这个合法排列的权值为 \(w_{p[1]} + 2w_{p[2]} + \ldots + nw_{p[n]}\)。你需要求出在所有合法排列中的最大权值。如果不存在合法排列,输出 \(-1\)。
样例解释中给出了合法排列和非法排列的实例。
对于前 \(20\%\) 的数据,\(1 \le n \le 10\);
对于前 \(40\%\) 的数据,\(1 \le n \le 15\);
对于前 \(60\%\) 的数据,\(1 \le n \le 1000\);
对于前 \(80\%\) 的数据,\(1 \le n \le 100000\);
对于 \(100\%\) 的数据,\(1 \le n \le 500000\),\(0 \le a_i \le n (1 \le i \le n)\),\(1 \le w_i \le 10^9\) ,所有 \(w_i\) 的和不超过 \(1.5 \times 10^{13}\)。
题解
这题的映射关系非常复杂好嘛!希望不要出现这种题目特别难懂的题目了!
把这题映射关系搞清楚后,发现就是\(a[i]->i\),然后在这棵树(有环无解)上按照拓扑序依次选完所有的点,贡献为选某点的时间×该点权值。
这样大概有40分的状压DP,但是考虑正解:
显然权值小的点要先选,那么权值最小的点在选完其父亲(如果有的话)后,一定马上被选。
考虑每个点向父亲缩,代价为父亲的siz×该点的val。于是各个联通块的权值如何确定呢?
考虑两个联通块AB,当前时刻为i,可以很轻松地列出\(W_{AB},W_{BA}\)的式子,相减发现\(\frac{\sum val}{siz}\)小的被先选会更优。
所以用一个set维护每个点,每次选取最小点向父亲合并,最后合成一个点就好了。
这题听说是YALI考过的题,HNOI考完走出考场听到许多“YALI人AK了”之类的言语,不是很爽快——HNOI有YALI学长出的题。当然不可否认的是YALI确实很强,应该也没有泄题的情况。但是我总觉得在这种大赛搬原题是一种极其不负责任的表现。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
const int N=1e6+10;
int n,m,q,key[N],l[N],r[N],t[N<<2];
int sta[N],top,LS[N],RS[N];
void build(int x,int l,int r)
{
if(l==r) {t[x]=key[l];return;}
int mid=(l+r)>>1;
build(x<<1,l,mid);
build(x<<1|1,mid+1,r);
t[x]=max(t[x<<1],t[x<<1|1]);
}
int query(int x,int l,int r,int gl,int gr,int bs)
{
int mid=(l+r)>>1,res=0;
if(l>=gl&&r<=gr)
{
if(t[x]<=bs) return 0;
if(l==r) return l;
if(t[x<<1|1]>bs) return query(x<<1|1,mid+1,r,gl,gr,bs);
else return query(x<<1,l,mid,gl,gr,bs);
}
if(gr>mid) res=query(x<<1|1,mid+1,r,gl,gr,bs);
if(gl<=mid&&!res) res=query(x<<1,l,mid,gl,gr,bs);
return res;
}
int Find(int l,int r,int x)
{
if(l>r) return l;
int p=query(1,1,n,l,r,x);
return p?p+1:l;
}
int main()
{
cin>>n>>m>>q;
for(int i=1,x,y;i<=m;i++) scanf("%d%d",&x,&y),key[x]=y;
build(1,1,n);LS[1]=1,RS[n]=n;
for(int i=2;i<=n;i++) LS[i]=(key[i-1]&&key[i-1]<=i-1)?i:LS[i-1];
for(int i=n-1;i>=1;i--) RS[i]=(key[i]&&key[i]>i)?i:RS[i+1];
for(int i=n;i>=1;i--)
{
r[i]=i;l[i]=Find(LS[i],i-1,i);
while(r[i]<n&&(!key[r[i]]||(key[r[i]]>=l[i]&&key[r[i]]<=r[i])))
r[i]=r[r[i]+1],l[i]=Find(LS[i],l[i]-1,r[i]);
}
for(int i=1,x,y;i<=q;i++)
scanf("%d%d",&x,&y),puts((l[x]<=y&&r[x]>=y)?"YES":"NO");
}
D2T3 道路
题意
W 国的交通呈一棵树的形状。W 国一共有 \(n − 1\) 个城市和 \(n\) 个乡村,其中城市从 \(1\) 到 \(n − 1\) 编号,乡村从 \(1\) 到 \(n\) 编号,且 \(1\) 号城市是首都。道路都是单向的,本题中我们只考虑从乡村通往首都的道路网络。对于每一个城市,恰有一条公路和一条铁路通向这座城市。对于城市 \(i\),通向该城市的道路(公路或铁路)的起点,要么是一个乡村,要么是一个编号比 \(i\) 大的城市。没有道路通向任何乡村。除了首都以外,从任何城市或乡村出发只有一条道路;首都没有往外的道路。从任何乡村出发,沿着唯一往外的道路走,总可以到达首都。
W 国的国王小 W 获得了一笔资金,他决定用这笔资金来改善交通。由于资金有限,小 W 只能翻修 \(n − 1\) 条道路。小 W 决定对每个城市翻修恰好一条通向它的道路,即从公路和铁路中选择一条并进行翻修。小 W 希望从乡村通向城市可以尽可能地便利,于是根据人口调查的数据,小 W 对每个乡村制定了三个参数,编号为 \(i\) 的乡村的三个参数是 \(a_i\),\(b_i\) 和 \(c_i\)。假设从编号为 \(i\) 的乡村走到首都一共需要经过 \(x\) 条未翻修的公路与 \(y\) 条未翻修的铁路,那么该乡村的不便利值为
\]
在给定的翻修方案下,每个乡村的不便利值相加的和为该翻修方案的不便利值。
翻修 \(n − 1\) 条道路有很多方案,其中不便利值最小的方案称为最优翻修方案,小 W 自然希望找到最优翻修方案,请你帮助他求出这个最优翻修方案的不便利值。
共 \(20\) 组数据,编号为 \(1 ∼ 20\)。
对于编号 \(\le 4\) 的数据,\(n \le 20\);
对于编号为 \(5 \sim 8\) 的数据,\(a_i, b_i, c_i \le 5,n \le 50\);
对于编号为 \(9 \sim 12\) 的数据,\(n \le 2000\);
对于所有的数据,\(n \le 20000\),\(1 \le a_i, b_i \le 60\),\(1 \le c_i \le 10^9\),\(s_i, t_i\) 是 \([−n, −1] \cap (i, n − 1]\) 内的整数,任意乡村可以通过不超过 \(40\) 条道路到达首都。
题解
据说这题出题人想复杂了于是成为了普及题。。。验题人干嘛去了啊。。
然而我刚才苦苦思索十分钟还是忘记怎么做了(去年做的)。。就怕被降智啊!!!
设\(dp[x][a][b]\)表示\(x\)的子树内,到根还有a条没有修好的公路、b条没有修好的铁路的最小总代价。
没了。
代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
int read()
{
char ch=getchar();int h=0,t=1;
while(ch!='-'&&(ch>'9'||ch<'0'))ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch>='0'&&ch<='9'){h=h*10+ch-'0';ch=getchar();}
return h*t;
}
const int MAXN=40010;
int N,head[MAXN],cnt,L[MAXN],R[MAXN];
int A[MAXN],B[MAXN],C[MAXN];
ll dp[MAXN>>1][41][41];
struct edge{int next,to,w;}a[MAXN<<2];
void link(int x,int y,int w){a[++cnt]=(edge){head[x],y,w};head[x]=cnt;}
void Pre(int x,int fa)
{
for(int i=head[x];~i;i=a[i].next)
{
int S=a[i].to;
if(S==fa)continue;
L[S]=L[x],R[S]=R[x];
(a[i].w==1)?L[S]++:R[S]++;
Pre(S,x);
}
}
ll DP(int x,int i,int j)
{
if(x<=N) return dp[x][i][j];
return 1LL*C[x]*(A[x]+i)*(B[x]+j);
}
void DFS(int x,int fa)
{
int lc=0,rc=0;
for(int i=head[x];~i;i=a[i].next)
if(a[i].to!=fa){DFS(a[i].to,x);rc?lc=a[i].to:rc=a[i].to;}
if(!lc) return;
for(int i=0;i<=L[x];i++)
for(int j=0;j<=R[x];j++)
dp[x][i][j]=min(DP(lc,i+1,j)+DP(rc,i,j),DP(lc,i,j)+DP(rc,i,j+1));
}
int main()
{
N=read();
memset(head,-1,sizeof(head));
for(int i=1;i<N;i++)
{
int x=read(),y=read();
if(x<0)x=-x+N;
if(y<0)y=-y+N;
link(i,x,1);link(x,i,1);
link(i,y,2);link(y,i,2);
}
for(int i=1;i<=N;i++)
{
int pos=i+N;
A[pos]=read();
B[pos]=read();
C[pos]=read();
}
Pre(1,0);
DFS(1,0);
printf("%lld\n",dp[1][0][0]);
return 0;
}
后记
这套题目可以说是非常好、质量非常高的啦。
如果今年让我考这套题目的话,最好的成绩是30+40+75+60+40+100,然而算上联赛也只能踩队线。
可以说非常刺激了。
后天加油啊!
HNOI2018简要题解的更多相关文章
- Noip 2014酱油记+简要题解
好吧,day2T1把d默认为1也是醉了,现在只能期待数据弱然后怒卡一等线吧QAQ Day0 第一次下午出发啊真是不错,才2小时左右就到了233,在车上把sao和fate补掉就到了= = 然后到宾馆之后 ...
- Tsinghua 2018 DSA PA2简要题解
反正没时间写,先把简要题解(嘴巴A题)都给他写了记录一下. upd:任务倒是完成了,我也自闭了. CST2018 2-1 Meteorites: 乘法版的石子合并,堆 + 高精度. 写起来有点烦貌似. ...
- Codeforces 863 简要题解
文章目录 A题 B题 C题 D题 E题 F题 G题 传送门 简要题解?因为最后一题太毒不想写了所以其实是部分题解... A题 传送门 题意简述:给你一个数,问你能不能通过加前导000使其成为一个回文数 ...
- JXOI2018简要题解
JXOI2018简要题解 T1 排序问题 题意 九条可怜是一个热爱思考的女孩子. 九条可怜最近正在研究各种排序的性质,她发现了一种很有趣的排序方法: Gobo sort ! Gobo sort 的算法 ...
- BJOI2018简要题解
BJOI2018简要题解 D1T1 二进制 题意 pupil 发现对于一个十进制数,无论怎么将其的数字重新排列,均不影响其是不是 \(3\) 的倍数.他想研究对于二进制,是否也有类似的性质. 于是他生 ...
- CQOI2018简要题解
CQOI2018简要题解 D1T1 破解 D-H 协议 题意 Diffie-Hellman 密钥交换协议是一种简单有效的密钥交换方法.它可以让通讯双方在没有事先约定密钥(密码)的情况下,通过不安全的信 ...
- AtCoder ExaWizards 2019 简要题解
AtCoder ExaWizards 2019 简要题解 Tags:题解 link:https://atcoder.jp/contests/exawizards2019 很水的一场ARC啊,随随便便就 ...
- Comet OJ - Contest #2 简要题解
Comet OJ - Contest #2 简要题解 cometoj A 模拟,复杂度是对数级的. code B 易知\(p\in[l,r]\),且最终的利润关于\(p\)的表达式为\(\frac{( ...
- HNOI2019 简要题解
HNOI 2019 简要题解 没想到自己竟也能有机会写下这篇题解呢. LOJ Luogu Day1T1 鱼 枚举\(AD\)两点后发现\(BC\)与\(EF\)相对独立,因此只需要计算合法的\(BC\ ...
随机推荐
- (网页)jQuery UI 实例 - 日期选择器(Datepicker)
默认功能 日期选择器(Datepicker)绑定到一个标准的表单 input 字段上.把焦点移到 input 上(点击或者使用 tab 键),在一个小的覆盖层上打开一个交互日历.选择一个日期,点击页面 ...
- 常用的Git命令整理
之前一直忙于项目苦于没有时间总结,今天刚好有时间特来总结一下在工作中常用到的代码版本管理器Git.至于为什么要用Git?Git相比SVN有哪些好处?我就不多说了,前人已经总结的很好.今天主要介绍的是常 ...
- JQuery实战中遇到的两个小问题$(document).ready() 、bind函数的参数传递问题
一.$(document).ready() 与 window.onload的区别 1.执行时间 window.onload 必须等到页面内所有元素(包括图片 css js等)加载完毕后才会执行. $( ...
- 安装VisualSVN Server 报"Service 'VisualSVN Server' failed to start. Please check VisualSVN Server log in Event Viewer for more details"错误.原因是启动"VisualSVN Server"失败
安装VisualSVN Server 报"Service 'VisualSVN Server' failed to start. Please check VisualSVN Server ...
- [cb]SceneView 获取鼠标位置
扩展需求 在Scene视图中获取鼠标的位置 Demo 在Scene视图中,当鼠标点击时实例化一个Cube 重点部分 实现代码 using UnityEngine; using UnityEditor; ...
- 软件发布时的 GA、RC、Beta
今天在使用 ovirt 的时候,遇到了其 Pre-release 版本并看到如下版本号:ovirt-node-ng-image-update-4.2.7-0.1.rc1.el7.noarch.rpm ...
- C# 生成强命名程序集并添加到GAC
针对一些类库项目或用户控件项目(一般来说,这类项目最后编译生成的是一个或多个dll文件),在程序开发完成后,有时需要将开发的程序集(dll文件)安装部署到GAC(全局程序集缓存)中,以便其他的程序也可 ...
- Matplotlib:可视化颜色命名分类和映射颜色分类
Matplotlib中支持的所有颜色分类 映射颜色分类
- 【17】有关python面向对象编程的提高【多继承、多态、类属性、动态添加与限制添加属性与方法、@property】
一.多继承 案例1:小孩继承自爸爸,妈妈.在程序入口模块再创建实例调用执行 #father模块 class Father(object): def __init__(self,money): self ...
- jQuery.form 的最新版本是 3.14
http://www.oschina.net/news/32628/jquery-form-3-14 有日子没跟进 jQuery.form 插件了,该插件已经从 2.xx 更新到 3.xx 了,目前最 ...