[JZOJ NOIP2018模拟10.21]
考试之前我刚刚领略到了特判的重要性,没想到T2的两个子任务还是写挂了,丢了20分
考试的感觉不行,一路打的都是暴力,正解的思路想到一半就断了推不下去
T1:逛公园
题目链接:
https://jzoj.net/senior/#contest/show/2534/0
题目:
琥珀色黄昏像糖在很美的远方,思念跟影子在傍晚一起被拉长……
小 B 带着 GF 去逛公园,公园一共有 n 个景点,标号为 1 . . . n。景点之间有 m 条路径相连。
小 B 想选择编号在一段区间 [l, r] 内的景点来游玩,但是如果这些景点的诱导子图形成了环,那么 GF 将会不高兴。
小 B 给出很多个询问 [x, y],想让你求有多少个区间 [l, r] 满足 x ≤ l, r ≤ y 且不会使 GF不高兴。
题解:
先来一遍tarjan把所有的点双拿出来,这张图就是环了。发现我们只需要知道环上节点编号的最大值和最小值就能判断当前区间是否包含这个环,问题转化为数轴上有一堆线段,每次询问一个区间不包含任何一条线段的方案数
我们设$lf$数组表示当前点能向左延伸的最远处,注意这个数组显然是单调增的,这个可以预处理出来。怎么预处理呢?对于每条线段$[l,r]$,我们令$lf[r]=l+1$,然后从左到右$lf[1]=1$,$lf[i]=max(lf[i],lf[i-1])(i>2)$
这样我们对于一组询问就可以统计答案了。若当前询问区间为$[x,y]$,假设其中的一个位置$i$,若满足$lf[i]>=x$,那么它对答案的贡献就是$i-lf[i]+1$;若满足$lf[i]<x$,那么对答案的贡献就是$i-x+1$
由于lf数组具有单调性,我们可以二分出一个位置i满足$lf[i]>=x,lf[i-1]<x$,对于$[i,y]$我们显然可以通过前缀和优化$O(1)$查询,对于$[x,i-1]$我们发现是一个等差数列,贡献是$\frac{(i-x+1)\times(i-x)}{2}$
当然若是二分出来的$i$大于$y$,那么$[x,y]$的任意一个区间都是合法的,这个需要特判一下
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
typedef long long ll;
const int N=3e5+15;
const int inf=1e9;
int n,m,tot=1,tp,tim,cnt;
int head[N],dfn[N],low[N],sta[N],lf[N];
ll rsum[N];
struct EDGE{
int to,nxt;
}edge[N<<1];
struct node{
int mx,mi;
}vd[N];
inline int read(){
char ch=getchar();int s=0,f=1;
while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9') {s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
return s*f;
}
void link(int u,int v){edge[++tot]=(EDGE){v,head[u]};head[u]=tot;}
void chkmax(int &a,int b){if (b>a) a=b;}
void chkmin(int &a,int b){if (b<a) a=b;}
void tarjan(int x,int pre){
dfn[x]=low[x]=++tim;sta[++tp]=x;
for (int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;
if (!dfn[y]){
tarjan(y,x);
chkmin(low[x],low[y]);
if (low[y]>dfn[x]) tp--;//这个必须要
if (dfn[x]==low[y]){
for (++cnt,vd[cnt].mx=vd[cnt].mi=x;sta[tp]!=x;tp--) chkmax(vd[cnt].mx,sta[tp]),chkmin(vd[cnt].mi,sta[tp]);
}
}
else if (y!=pre) chkmin(low[x],dfn[y]);
}
}
int main(){
freopen("graph.in","r",stdin);
freopen("graph.out","w",stdout);
n=read();m=read();
for (int i=1,u,v;i<=m;i++){
u=read();v=read();
link(u,v);link(v,u);
}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i,-1);
for (int i=1;i<=cnt;i++) lf[vd[i].mx]=vd[i].mi+1;
lf[1]=1;rsum[1]=1;
for (int i=2;i<=n;i++) chkmax(lf[i],lf[i-1]);
for (int i=1;i<=n;i++) rsum[i]=i-lf[i]+1+rsum[i-1];
int q=read();
while (q--)
{
int x=read(),y=read();
int pos=lower_bound(lf+1,lf+1+n,x)-lf;
ll re;
if (pos<=y)
{
re=rsum[y]-rsum[pos-1];
re+=1ll*(pos-x)*(pos-x+1)/2;
}
else re=1ll*(y-x+1)*(y-x+2)/2;
printf("%lld\n",re);
}
return 0;
}
T2:风筝 (CF650D)
题目链接:
https://jzoj.net/senior/#contest/show/2534/1
题目:
当一阵风吹来,风筝飞上天空,为了你,而祈祷,而祝福,而感动……
oyiya 在 AK 了 IOI 之后来到了乡下,在田野中玩耍,放松身心。
他发现前面有一排小朋友在放风筝,每一个风筝有一个高度 hi,风筝的高度可能会随着小朋友的心情而改变。这时,毒瘤的 oyiya 有了一个毒瘤的 idea,他想知道改变高度之后风筝的最长严格上升子序列。oyiya 太强了表示并不想做这种水题,你能解决这个问题吗?
题解:
推荐大佬博客:https://blog.csdn.net/zearot/article/details/50857353,感谢大佬
其实我觉得这篇博客讲的很清楚了...我再讲有点冗余
其实就是分修改后两种情况,一种是b在LIS中,一种是b不在LIS中
若b在LIS中,维护一下a之前的可以转移到b的最大值,a之后的可以从b转移到的最大值。离线加树状数组的话其实很好写,就是遇到询问就在当前树状数组里查询最大值,然后再把当前元素加进树状数组。当然还可以写可持久化线段树
若b不在LIS中,判断一下少了下标a会不会影响原来的LIS,即判断下标a是否一定要出现在原来的LIS中。首先知道一个结论,对于一个序列,若一个下标多次出现在LIS中,那么这个下标在LIS的排名一定是一样的。那么就记录一个cunt数组判断有几个位置可以胜任排名i就好了
两种情况的答案取max
离线树状数组做法
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int N=1e6+15;
int n,m,len,tot;
int h[N],L[N],R[N],c[N],t[N],cunt[N],a[N],b[N],ANSl[N],ANSr[N];
struct node{
int ca,id;
};
vector <node> q[N];
inline int read(){
char ch=getchar();int s=0,f=1;
while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9') {s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
return s*f;
}
void init(){
sort(c+1,c+1+tot);
len=unique(c+1,c+1+tot)-c-1;
for (int i=1;i<=n;i++) h[i]=lower_bound(c+1,c+1+len,h[i])-c;
for (int i=1;i<=m;i++) b[i]=lower_bound(c+1,c+1+len,b[i])-c;
}
void chkmax(int &a,int b) {if (b>a) a=b;}
void modify(int x,int y){
while (x<=len){
chkmax(t[x],y);
x+=x&-x;
}
}
int query(int x){
int re=0;
while (x){
chkmax(re,t[x]);
x-=x&-x;
}
return re;
}
int main(){
freopen("kite.in","r",stdin);
freopen("kite.out","w",stdout);
n=read();m=read();
for (int i=1;i<=n;i++) h[i]=read(),c[++tot]=h[i];
for (int i=1;i<=m;i++) a[i]=read(),b[i]=read(),c[++tot]=b[i];
init();
//for (int i=1;i<=n;i++) printf("%d ",h[i]);
//printf("\n");
int k=0;
for (int i=1;i<=n;i++){
L[i]=max(1,query(h[i]-1)+1);
chkmax(k,L[i]);
modify(h[i],L[i]);
}
memset(t,0,sizeof(t));
for (int i=n;i;i--){
R[i]=query(len-h[i]+1)+1;
modify(len-h[i]+1,R[i]);
}
//for (int i=1;i<=n;i++) printf("%d %d\n",L[i],R[i]);
memset(t,0,sizeof(t));
for (int i=1;i<=m;i++) q[a[i]].push_back((node){b[i],i});
for (int i=1;i<=n;i++){
for (int j=0;j<q[i].size();j++)
ANSl[q[i][j].id]=query(q[i][j].ca-1);
modify(h[i],L[i]);
}
memset(t,0,sizeof(t));
for (int i=n;i;i--){
for (int j=0;j<q[i].size();j++)
ANSr[q[i][j].id]=query(len-q[i][j].ca);
modify(len-h[i]+1,R[i]);
}
for (int i=1;i<=n;i++) if (L[i]+R[i]==k+1) cunt[L[i]]++;
for (int i=1;i<=m;i++)
{
if (L[a[i]]+R[a[i]]!=k+1)
{
printf("%d\n",max(k,ANSl[i]+ANSr[i]+1));
}
else
{
if (cunt[L[a[i]]]>1) printf("%d\n",max(k,ANSl[i]+ANSr[i]+1));
else printf("%d\n",max(k-1,ANSl[i]+ANSr[i]+1));
}
}
return 0;
}
在线主席树做法
#include<algorithm> #include<cstring> #include<cstdio> #include<iostream> using namespace std; const int N=1e6+15; int n,m,len,tot,cnt; int h[N],L[N],R[N],c[N],t[N],cunt[N],a[N],b[N],ANSl[N],ANSr[N]; int rt[N<<4],lx[N<<4],rx[N<<4],mx[N<<4]; struct node{ int ca,id; }; inline int read(){ char ch=getchar();int s=0,f=1; while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9') {s=(s<<3)+(s<<1)+ch-'0';ch=getchar();} return s*f; } void init(){ sort(c+1,c+1+tot); len=unique(c+1,c+1+tot)-c-1; for (int i=1;i<=n;i++) h[i]=lower_bound(c+1,c+1+len,h[i])-c; for (int i=1;i<=m;i++) b[i]=lower_bound(c+1,c+1+len,b[i])-c; } void chkmax(int &a,int b) {if (b>a) a=b;} void modify(int x,int y){ while (x<=len){ chkmax(t[x],y); x+=x&-x; } } int query(int x){ int re=0; while (x){ chkmax(re,t[x]); x-=x&-x; } return re; } void pushup(int o){ mx[o]=max(mx[lx[o]],mx[rx[o]]); } int update(int pre,int l,int r,int x,int y){ int o=++cnt; if (l==r) {mx[o]=y;return o;} lx[o]=lx[pre];rx[o]=rx[pre];mx[o]=mx[pre]; int mid=l+r>>1; if (x<=mid) lx[o]=update(lx[pre],l,mid,x,y); else rx[o]=update(rx[pre],mid+1,r,x,y); pushup(o); return o; } int query1(int o,int l,int r,int x){ if (!o) return 0; if (x>=r) return mx[o]; if (l>x) return 0; int mid=l+r>>1; return max(query1(lx[o],l,mid,x),query1(rx[o],mid+1,r,x)); } int query2(int o,int l,int r,int x){ if (!o) return 0; if (x<=l) return mx[o]; if (r<x) return 0; int mid=l+r>>1; return max(query2(lx[o],l,mid,x),query2(rx[o],mid+1,r,x)); } int main(){ freopen("kite.in","r",stdin); freopen("kite.out","w",stdout); n=read();m=read(); for (int i=1;i<=n;i++) h[i]=read(),c[++tot]=h[i]; for (int i=1;i<=m;i++) a[i]=read(),b[i]=read(),c[++tot]=b[i]; init(); int k=0; for (int i=1;i<=n;i++){ L[i]=max(1,query(h[i]-1)+1); chkmax(k,L[i]); modify(h[i],L[i]); } memset(t,0,sizeof(t)); for (int i=n;i;i--){ R[i]=query(len-h[i]+1)+1; modify(len-h[i]+1,R[i]); } memset(rt,0,sizeof(rt));cnt=0; for (int i=1;i<=n;i++) rt[i]=update(rt[i-1],1,len,h[i],L[i]); for (int i=1;i<=m;i++) ANSl[i]=query1(rt[a[i]-1],1,len,b[i]-1); memset(rt,0,sizeof(rt));cnt=0; for (int i=n;i;i--) rt[i]=update(rt[i+1],1,len,h[i],R[i]); for (int i=1;i<=m;i++) ANSr[i]=query2(rt[a[i]+1],1,len,b[i]+1); for (int i=1;i<=n;i++) if (L[i]+R[i]==k+1) cunt[L[i]]++; for (int i=1;i<=m;i++) { if (L[a[i]]+R[a[i]]!=k+1) { printf("%d\n",max(k,ANSl[i]+ANSr[i]+1)); } else { if (cunt[L[a[i]]]>1) printf("%d\n",max(k,ANSl[i]+ANSr[i]+1)); else printf("%d\n",max(k-1,ANSl[i]+ANSr[i]+1)); } } return 0; }
T3:种花
题目链接:
http://172.16.0.132/senior/#main/show/5921
题目:
小 H 是一个喜欢养花的女孩子。
她买了 n 株花,编号为一里香,二里香……七里香……n 里香,她想把这些花分别种在 n个不同的花盆里。
对于一种方案,第 i 个花盆里种的是 ai 里香,小 H 定义其美丽值为:
第一行一个整数 n,第二行有 n 个整数表示 {pi},表示第i个花盆不能种pi里香。
求所有合法方案的美丽值之和
题解:
很显然这是道毒瘤的计数题,不能直接根据定义计算,我们应该计算每一部分的贡献
先不管这个,我们先定义$f_{x,y}$表示表示一共有(x+y)个元素,其中y个没有限制,另外x个被禁止放入一个位置的方案数,那么有转移:
$f_{0,0}=1$
$f_{x,0}=(x-1)\times(f_{x-1,0}+f_{x-2,0})$(错排公式)
$f_{x,y}=x \times f_{x-1,y}+y \times f_{x,y-1}$
前面两个比较显然,考虑最后一个的含义。考虑新加入一个没有限制的元素。如果该元素放置的位置是有限制的,那么这个位置以前有限制的元素就变成了没有限制的了,有x种选择方法,剩下的元素的放置方案数为$f_{x-1,y}$;如果该元素放置的位置是没有限制的,那么有y种选择方法,剩下的元素的放置方案数为$f_{x,y-1}$。
我们考虑枚举两个位置$O(1)$计算贡献,假设我们枚举的位置是i和j,分以下三种情况讨论
1. 若 $p_j$在$i$ 上, $p_i$在$j$上,那么有$f_{n−2,0}$ 种情况。贡献为 $a_1 = max(0, p_j − p_i)$。
2. 若上面两个条件只满足一个,那么有 $f_{n−3,1}$ 种情况(假设只满足$p_j$在i上,那么当$p_i$随便填一个位置,剩下的$n-2$个数就会有一个没有限制)。贡献为$a_2=\sum_{k=1}^{p_j-1}-a_1+\sum_{k=1}^{n-p_i}-a_1$。(分 别考虑$ j $在 $p_i $和 $i$ 在 $p_j$ 并减去$ i, j$ 同时在 $p_j , p_i $的情况)
3. 两个条件都不满足的有$f_{n−4,2}$种情况($p_i$和$p_j$都随便填一个位置,剩下的$n-2$个数就会有两个没有限制)。贡献为$a_3=\sum_{i=2}^{n}\sum_{j=1}^{i-1}j-a_1-a_2-(\sum_{i=1}^{p_i-1}i+\sum_{i=1}^{n-p_j}i-max(0,p_i-p_j)) $
所以答案为$\sum_{i<j} (j-i)(a_1 f_{n-2,0}+a_2 f{n-3,1}+a_3 f_{n-4,2})$
#include<algorithm> #include<cstring> #include<cstdio> #include<iostream> using namespace std; typedef long long ll; const int N=6e3+15; const ll mo=1e9+9; int n; int p[N]; ll f[N][3]; inline int read(){ char ch=getchar();int s=0,f=1; while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9') {s=(s<<3)+(s<<1)+ch-'0';ch=getchar();} return s*f; } ll qpow(ll a,ll b){ ll re=1; for (;b;b>>=1,a=a*a%mo) if (b&1) re=re*a%mo; return re; } ll inv(ll x) {return qpow(x,mo-2);} int main(){ freopen("derangement.in","r",stdin); freopen("derangement.out","w",stdout); n=read(); for (int i=1;i<=n;i++) p[i]=read(); f[0][0]=1;f[1][0]=0; for (int i=1;i<3;i++) f[0][i]=f[0][i-1]*i%mo; for (int i=2;i<=n;i++) f[i][0]=(i-1)*(f[i-1][0]+f[i-2][0])%mo; for (int i=1;i<=2;i++) for (int j=1;j<=n;j++) f[j][i]=(j*f[j-1][i]%mo+i*f[j][i-1]%mo)%mo; ll ans=0,inv12=inv(12),inv2=inv(2),inv4=inv(4); for (int i=1;i<n;i++) for (int j=i+1;j<=n;j++) { ll a1=max(0,p[j]-p[i]); ll a2=(p[j]*(p[j]-1)%mo*inv2%mo+(n-p[i]+1)*(n-p[i])%mo*inv2%mo-2*a1%mo+mo)%mo; ll a3=((n*(n+1)%mo*(2*n+1)%mo*inv12%mo-n*(n+1)%mo*inv4%mo+mo-a1-a2-p[i]*(p[i]-1)%mo*inv2%mo-(n-p[j]+1)*(n-p[j])%mo*inv2%mo+max(0,p[i]-p[j]))%mo+mo)%mo; ans=(ans+(j-i)*(a1*f[n-2][0]%mo+a2*f[n-3][1]%mo+a3*f[n-4][2]%mo)%mo)%mo; } printf("%lld\n",ans); return 0; }