2017.11.2 Problem A
做法: 这题按照题意大模拟,题目太繁琐就不贴了…
2017.11.2 Problem B
题目大意: 对于一个排列,在每两个相邻的数写上符号,如果右边的大于左边的写
<
<
<,否则写
>
>
>,问有多少种
1
1
1~
n
(
≤
1000
)
n(\le 1000)
n(≤1000)的排列中恰好有
k
k
k个
<
<
<?结果对大素数取模,多组询问。
做法: 本题需要用到DP。
令
f
(
i
,
j
)
f(i,j)
f(i,j)为
1
1
1 ~
i
i
i的排列中恰有
k
k
k个
<
<
<的方案数,我们分析
f
(
i
,
j
)
f(i,j)
f(i,j)可从哪两个状态继承方案数。我们假设这时已经求出了
f
(
i
−
1
,
x
)
f(i-1,x)
f(i−1,x),那么我们可以这样思考:往
1
1
1~
i
−
1
i-1
i−1的排列中某一个位置加入
i
i
i,使得最后有
j
j
j个
<
<
<,能有多少种方法?注意到,插入有4种情况:
1.插入在数列首位,由于
i
>
i
−
1
i>i-1
i>i−1,所以这样插入
<
<
<数量不变。
2.插入在数列末尾,由于
i
>
i
−
1
i>i-1
i>i−1,所以这样插入
<
<
<数量增加
1
1
1。
3.插入在原来
<
<
<号的位置,由于
i
>
i
−
1
i>i-1
i>i−1,所以插入后状态会从
.
.
.
<
.
.
.
...<...
...<...变为
.
.
.
<
i
>
.
.
.
...<i>...
...<i>...,所以这样插入
<
<
<数量不变,共有
j
j
j个位置可以插入。
4.插入在原来
>
>
>号的位置,由于
i
>
i
−
1
i>i-1
i>i−1,所以插入后状态会从
.
.
.
>
.
.
.
...>...
...>...变为
.
.
.
<
i
>
.
.
.
...<i>...
...<i>...,所以这样插入
<
<
<数量增加
1
1
1,共有
i
−
j
−
1
i-j-1
i−j−1个位置可以插入。
综上所述,有
i
−
j
i-j
i−j种插入方法使
<
<
<数量增加
1
1
1,有
j
+
1
j+1
j+1种插入方法使
<
<
<数量不变,那么易得状态转移方程:
f
(
i
,
j
)
=
(
i
−
j
)
f
(
i
−
1
,
j
−
1
)
+
(
j
+
1
)
f
(
i
−
1
,
j
)
f(i,j)=(i-j)f(i-1,j-1)+(j+1)f(i-1,j)
f(i,j)=(i−j)f(i−1,j−1)+(j+1)f(i−1,j)
O
(
n
2
)
O(n^2)
O(n2)处理出来存在数组里,再
O
(
1
)
O(1)
O(1)回答询问即可。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define mod 1000000007
using namespace std;
int T,n,k;
ll f[2010][2010];
bool vis[21]={0};
int main()
{
memset(f,0,sizeof(f));
f[1][0]=1;
for(ll i=2;i<=2000;i++)
{
f[i][0]=1;
for(ll j=1;j<i;j++)
f[i][j]=((i-j)*f[i-1][j-1]+(j+1)*f[i-1][j])%mod;
}
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&k);
printf("%lld\n",f[n][k]);
}
return 0;
}
2017.11.2 Problem C
题目大意: 有
n
(
≤
200000
)
n(\le 200000)
n(≤200000)只猴子,
1
1
1号猴子挂在树上,每只猴子有两只手,可以抓住其他猴子,也可以被其他猴子抓住,现在有
m
(
≤
400000
)
m(\le 400000)
m(≤400000)个时刻,每个时刻都会有一只猴子放掉一只手,使得某些猴子落地,求出每一只猴子落地的时刻。
做法: 本题需要用到并查集+反向处理。
我们可以把题目的模型理解为:一个图有
n
n
n个点和不超过
2
n
2n
2n条边,每条边有一个断开时刻(如果到最后都不断开,断开时刻就是
i
n
f
inf
inf),求每个点最早什么时刻开始不与点
1
1
1连通、注意到要求的答案就是该点到点
1
1
1路径上边权最小值的最大值,因此想到用最大生成树。因为边实际上已经相当于给我们排好序了,所以我们按照操作顺序的反序加边构造最大生成树,然后在最大生成树上DFS一遍就可以求出答案了,时间复杂度
O
(
n
+
m
)
O(n+m)
O(n+m)。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define mod 1000000007
using namespace std;
int T,n,k;
ll f[2010][2010];
bool vis[21]={0};
int main()
{
memset(f,0,sizeof(f));
f[1][0]=1;
for(ll i=2;i<=2000;i++)
{
f[i][0]=1;
for(ll j=1;j<i;j++)
f[i][j]=((i-j)*f[i-1][j-1]+(j+1)*f[i-1][j])%mod;
}
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&k);
printf("%lld\n",f[n][k]);
}
return 0;
}
2017.11.4 Problem A
题目大意: 平面上有
n
n
n个点,需要维护一个数据结构,支持:将第
l
l
l到
r
r
r个点的
x
x
x坐标或
y
y
y坐标增加或减少一个量,询问第
l
l
l到
r
r
r个点两两之间曼哈顿距离的最大值。
做法: 本题需要用到线段树。
因为我们要求
max
(
∣
x
i
−
x
j
∣
+
∣
y
i
−
y
j
∣
)
\max(|x_i-x_j|+|y_i-y_j|)
max(∣xi−xj∣+∣yi−yj∣),那么我们不妨设
x
i
>
x
j
x_i>x_j
xi>xj,所以当
y
i
≥
y
j
y_i\ge y_j
yi≥yj时,答案为
(
x
i
+
y
i
)
−
(
x
j
+
y
j
)
(x_i+y_i)-(x_j+y_j)
(xi+yi)−(xj+yj),当
y
i
<
y
j
y_i<y_j
yi<yj时,答案为
(
x
i
−
y
i
)
−
(
x
j
−
y
j
)
(x_i-y_i)-(x_j-y_j)
(xi−yi)−(xj−yj),可以证明,这两个值的最大值取某一个时,
y
i
y_i
yi和
y
j
y_j
yj的大小关系绝对满足最大值存在的那个关系。因此用线段树维护
x
i
+
y
i
x_i+y_i
xi+yi和
x
i
−
y
i
x_i-y_i
xi−yi的最大和最小值,这样就可以解决这个问题了。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define inf 1000000000
using namespace std;
int n,m,x[100010],y[100010];
int seg[400010][4],p[400010][4],ans[4];
void pushdown(int no)
{
for(int i=0;i<=3;i++)
if (p[no][i]!=0)
{
p[no<<1][i]+=p[no][i],p[no<<1|1][i]+=p[no][i];
seg[no<<1][i]=seg[no<<1][i]+p[no][i];
seg[no<<1|1][i]=seg[no<<1|1][i]+p[no][i];
p[no][i]=0;
}
}
void pushup(int no)
{
seg[no][0]=max(seg[no<<1][0],seg[no<<1|1][0]);
seg[no][1]=min(seg[no<<1][1],seg[no<<1|1][1]);
seg[no][2]=max(seg[no<<1][2],seg[no<<1|1][2]);
seg[no][3]=min(seg[no<<1][3],seg[no<<1|1][3]);
}
void buildtree(int no,int l,int r)
{
if (l==r)
{
seg[no][0]=seg[no][1]=x[l]+y[l];
seg[no][2]=seg[no][3]=x[l]-y[l];
return;
}
int mid=(l+r)>>1;
buildtree(no<<1,l,mid);
buildtree(no<<1|1,mid+1,r);
pushup(no);
}
void modify(int no,int l,int r,int s,int t,int d,bool mode)
{
if (l>=s&&r<=t)
{
seg[no][0]+=d,seg[no][1]+=d,p[no][0]+=d,p[no][1]+=d;
if (mode) seg[no][2]-=d,seg[no][3]-=d,p[no][2]-=d,p[no][3]-=d;
else seg[no][2]+=d,seg[no][3]+=d,p[no][2]+=d,p[no][3]+=d;
return;
}
pushdown(no);
int mid=(l+r)>>1;
if (s<=mid) modify(no<<1,l,mid,s,t,d,mode);
if (t>mid) modify(no<<1|1,mid+1,r,s,t,d,mode);
pushup(no);
}
void query(int no,int l,int r,int s,int t)
{
if (l>=s&&r<=t)
{
ans[0]=max(ans[0],seg[no][0]);
ans[1]=min(ans[1],seg[no][1]);
ans[2]=max(ans[2],seg[no][2]);
ans[3]=min(ans[3],seg[no][3]);
return;
}
int mid=(l+r)>>1;
pushdown(no);
if (s<=mid) query(no<<1,l,mid,s,t);
if (t>mid) query(no<<1|1,mid+1,r,s,t);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&x[i],&y[i]);
buildtree(1,1,n);
while(m--)
{
int op,l,r,d;
scanf("%d",&op);
if (op==1)
{
scanf("%d%d%d",&l,&r,&d);
modify(1,1,n,l,r,d,0);
}
if (op==2)
{
scanf("%d%d%d",&l,&r,&d);
modify(1,1,n,l,r,d,1);
}
if (op==3)
{
scanf("%d%d",&l,&r);
ans[0]=ans[2]=-inf,ans[1]=ans[3]=inf;
query(1,1,n,l,r);
printf("%d\n",max(ans[0]-ans[1],ans[2]-ans[3]));
}
}
return 0;
}
2017.11.4 Problem B
题目大意: 平面上有
n
n
n个点,每个点有一个颜色,问在不同颜色的点对之间连直线的最大斜率。
做法: 本题需要用到二分答案+线段树。
考虑二分斜率,接下来的问题就是判定这个斜率是大了还是小了。我们可以将点先按
x
x
x坐标从小到大排序,然后过每个点作一条斜率为当前二分到的值的直线,从左往右处理点,如果在这个点前面有和这个点颜色不同的点在这条直线之下,那么就存在更大的斜率(过那个点和当前点的直线斜率肯定比当前斜率大),如果处理完全部点后都不存在这种情况,那么表示当前的斜率就大于等于最终答案。怎么判断有没有点在直线之下呢?答案是有的。我们算出每个点对应直线的截距,当处理到一个点时,如果前面有和它颜色不同的点,过它的直线的截距比过当前点直线的截距要小,就说明它在过当前点直线的下面。那么我们只需求出:在当前点之前的,颜色与当前点不同的截距最小值。我们可以对颜色建线段树,然后转化为两个区间询问即可。这样就解决了这个问题。
然而这道题卡精度,代码就不贴了…
2017.11.4 Problem C
题目大意: 一棵树有
n
n
n个点,每条边有边权,
q
q
q个询问,每次询问树上有多少条路径满足边权异或和为
x
x
x。
做法: 本题需要用到FWT(快速沃尔什变换),然而我并不会,就写一下部分分做法吧。
首先,树上一条路径的异或和可以转化为两个端点到根的路径异或和的异或,因为从LCA到根的路径被算了两次抵消掉了。那么我们可以求出从根到每个点的路径和,然后每次询问,求出有多少对数字异或起来值为
x
x
x即可。怎么处理呢?用前
f
[
i
]
f[i]
f[i]记录数字
i
i
i出现的次数,然后对于一个数
n
o
w
now
now,和它异或起来值为
x
x
x的数只有
n
o
w
now
now
x
o
r
xor
xor
x
x
x,那么答案累加上
f
[
n
o
w
f[now
f[now
x
o
r
xor
xor
x
]
x]
x]即可。这样做时间复杂度为
O
(
n
q
)
O(nq)
O(nq),本题中可以过60分。
以下是本人代码(60分):
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
int n,q,tot=0,first[100010]={0},sum[100010];
ll vis[2000010]={0};
struct edge {int v,next,d;} e[200010];
void insert(int a,int b,int d)
{
e[++tot].v=b;
e[tot].next=first[a];
e[tot].d=d;
first[a]=tot;
}
void dfs(int v,int f,int s)
{
sum[v]=s;
for(int i=first[v];i;i=e[i].next)
if (e[i].v!=f) dfs(e[i].v,v,s^e[i].d);
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
insert(a,b,w),insert(b,a,w);
}
dfs(1,0,0);
for(int i=1;i<=q;i++)
{
ll ans=0;
int x;
scanf("%d",&x);
for(int i=1;i<=n;i++)
{
ans+=vis[sum[i]^x];
vis[sum[i]]++;
}
for(int i=1;i<=n;i++) vis[sum[i]]--;
printf("%lld\n",ans);
}
return 0;
}
2017.11.5 Problem A
题目大意: 给定一个长为
n
n
n的小写字母组成的字符串,
m
m
m次询问,每次询问一个区间中字典序最大的子序列长度。
做法: 本题需要用到前缀和(傻逼做法:线段树(本人就是这么做的…))。
考虑怎么取才能使子序列字典序最大,那么肯定从
z
z
z搜到
a
a
a,每次都取完所有的最大字母,然后跳到这个字母最后出现的位置之后,直到没办法取为止。那么用
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示区间
[
1
,
i
]
[1,i]
[1,i]中,字符
j
j
j出现的次数,用
g
[
i
]
[
j
]
g[i][j]
g[i][j]表示区间
[
1
,
i
]
[1,i]
[1,i]中,字符
j
j
j最后出现的位置,然后每次询问就可以
O
(
26
)
O(26)
O(26)解决了,总的时间复杂度为
O
(
26
n
+
26
m
)
O(26n+26m)
O(26n+26m)。
当然,如果你闲的慌,可以和我一样用线段树维护前缀和和最后出现位置…
以下是本人代码(线段树):
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n,q,totsum,totrht;
char s[100010],totmx;
struct node
{
char mx;
int sum,rht;
}seg[400010];
void pushup(int no)
{
seg[no].mx=max(seg[no<<1].mx,seg[no<<1|1].mx);
if (seg[no<<1].mx>seg[no<<1|1].mx) seg[no].rht=seg[no<<1].rht;
else seg[no].rht=seg[no<<1|1].rht;
if (seg[no<<1].mx==seg[no<<1|1].mx) seg[no].sum=seg[no<<1].sum+seg[no<<1|1].sum;
else if (seg[no<<1].mx>seg[no<<1|1].mx) seg[no].sum=seg[no<<1].sum;
else seg[no].sum=seg[no<<1|1].sum;
}
void buildtree(int no,int l,int r)
{
if (l==r)
{
seg[no].mx=s[l];
seg[no].sum=1;
seg[no].rht=l;
return;
}
int mid=(l+r)>>1;
buildtree(no<<1,l,mid);
buildtree(no<<1|1,mid+1,r);
pushup(no);
}
void query(int no,int l,int r,int s,int t)
{
if (l>=s&&r<=t)
{
if (seg[no].mx>totmx)
{
totmx=seg[no].mx;
totsum=seg[no].sum;
totrht=seg[no].rht;
}
else if (seg[no].mx==totmx)
{
totsum+=seg[no].sum;
totrht=seg[no].rht;
}
return;
}
int mid=(l+r)>>1;
if (s<=mid) query(no<<1,l,mid,s,t);
if (t>mid) query(no<<1|1,mid+1,r,s,t);
}
int main()
{
scanf("%d%d",&n,&q);
scanf("%s",s+1);
buildtree(1,1,n);
for(int i=1;i<=q;i++)
{
int ans=0,x,y;
scanf("%d%d",&x,&y);
while(x<=y)
{
totsum=totrht=totmx=0;
query(1,1,n,x,y);
ans+=totsum;
x=totrht+1;
}
printf("%d\n",ans);
}
return 0;
}
2017.11.5 Problem B
题目大意: 一棵有
n
n
n个节点的树,每个点是白色或黑色,求只包含一个黑点的连通块的数量。
做法: 本题需要用到树形DP+逆元。
用
f
[
i
]
f[i]
f[i]表示以
i
i
i为根的子树中,包含点
i
i
i的不含黑色点的连通块数目,用
g
[
i
]
g[i]
g[i]表示以
i
i
i为根的子树中,包含点
i
i
i的含一个黑色点的连通块数目,那么有状态转移方程(以下
j
j
j表示
i
i
i的每一个儿子):
当
i
i
i为黑色点时:
f
[
i
]
=
0
,
g
[
i
]
=
∏
j
(
f
[
j
]
+
1
)
f[i]=0,g[i]=\prod_j(f[j]+1)
f[i]=0,g[i]=∏j(f[j]+1)
其中
g
[
i
]
g[i]
g[i]的方程可理解为,每个分支都有
f
[
j
]
+
1
f[j]+1
f[j]+1种选择(选择一个连通块或不选择这个分支),根据乘法原理理解即可。
当
i
i
i为白色点时:
f
[
i
]
=
∏
j
(
f
[
j
]
+
1
)
,
g
[
i
]
=
∑
j
f
[
i
]
f
[
j
]
+
1
g
[
j
]
f[i]=\prod_j(f[j]+1),g[i]=\sum_j\frac{f[i]}{f[j]+1}g[j]
f[i]=∏j(f[j]+1),g[i]=∑jf[j]+1f[i]g[j]
其中
g
[
i
]
g[i]
g[i]的方程可理解为,枚举每个儿子
j
j
j,计算黑点在这个儿子中的方案数,然后加起来成为总的方案数。
这个方程的时间复杂度是
O
(
n
)
O(n)
O(n)的。因为最后结果要对大质数取模,所以期间除以某个数要变成乘以这个数对大质数的逆元,那么最后的时间复杂度应该为
O
(
n
log
d
)
O(n\log d)
O(nlogd),其中
d
d
d为大质数。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define mod 1000000007
using namespace std;
int n,tot=0,first[100010]={0};
ll f[100010],g[100010],ans=0;
bool black[100010]={0};
struct edge {int v,next;} e[200010];
void insert(int a,int b)
{
e[++tot].v=b;
e[tot].next=first[a];
first[a]=tot;
}
ll power(ll a,ll b)
{
ll s=1,ss=a;
while(b)
{
if (b&1) s=(s*ss)%mod;
b>>=1;ss=(ss*ss)%mod;
}
return s;
}
void dp(int v,int fa)
{
if (!black[v]) f[v]=1,g[v]=0;
else f[v]=0,g[v]=1;
for(int i=first[v];i;i=e[i].next)
if (e[i].v!=fa)
{
dp(e[i].v,v);
if (black[v]) g[v]=(g[v]*(1+f[e[i].v]))%mod;
else f[v]=(f[v]*(1+f[e[i].v]))%mod;
}
if (!black[v])
{
for(int i=first[v];i;i=e[i].next)
if (e[i].v!=fa) g[v]=(g[v]+(f[v]*power(1+f[e[i].v],mod-2)%mod*g[e[i].v]))%mod;
}
ans=(ans+g[v])%mod;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int a,b;
scanf("%d%d",&a,&b);
insert(a,b),insert(b,a);
}
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
black[i]=x;
}
dp(1,0);
printf("%lld",ans);
return 0;
}
2017.11.5 Problem C
题目大意: 平面上有
n
(
≤
200
)
n(\le 200)
n(≤200)个点,每个点有一个颜色,问在每种颜色的点中等概率选一个点,选出的点集的凸包的期望面积。
做法: 本题需要用到数学期望+向量叉乘。
我们知道求任意简单多边形的面积都可以用这个公式(当然,点必须是按逆时针顺序环绕给出的):
S
=
1
2
∑
i
c
r
o
s
s
(
p
i
,
p
n
e
x
t
)
S=\frac{1}{2}\sum_icross(p_i,p_{next})
S=21∑icross(pi,pnext)
其中
c
r
o
s
s
cross
cross表示两个向量的叉积,
n
e
x
t
next
next为点
i
i
i按逆时针顺序的下一个点。因为这个公式对所有简单多边形都适用,所以对于凸多边形也当然适用。注意到上面的公式只和一些有序点对有关,启发我们求每个有序点对对答案的贡献。对于一个有序点对
(
p
i
,
p
j
)
(p_i,p_j)
(pi,pj),它对答案的贡献应该是:
1
2
×
有
向
边
(
p
i
,
p
j
)
在
凸
包
上
的
概
率
×
c
r
o
s
s
(
p
i
,
p
j
)
\frac{1}{2}\times 有向边(p_i,p_j)在凸包上的概率\times cross(p_i,p_j)
21×有向边(pi,pj)在凸包上的概率×cross(pi,pj)。计算一条有向边在凸包上的概率,就是说,在它右边的点都不取,而一定取这条边的两个端点的概率,用
O
(
n
)
O(n)
O(n)统计计算一下概率即可,这样算法总的时间复杂度就是
O
(
n
3
)
O(n^3)
O(n3)。
以下是本人代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#define eps 1e-8
using namespace std;
int n,m,P[210][210];
double ans=0.0,d,num[210],tot[210]={0};
struct point
{
double x,y;
int c;
point operator - (point a) const
{
point now;
now.x=x-a.x;
now.y=y-a.y;
return now;
}
}p[210],s[210];
double multi(point a,point b)
{
return a.x*b.y-a.y*b.x;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int c;
scanf("%lf%lf%d",&p[i].x,&p[i].y,&p[i].c);
tot[p[i].c]=tot[p[i].c]+1.0;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if (i!=j&&p[i].c!=p[j].c)
{
double f=1.0;
memset(num,0,sizeof(num));
num[p[i].c]=num[p[j].c]=1.0;
for(int k=1;k<=n;k++)
if (i!=k&&j!=k&&p[k].c!=p[i].c&&p[k].c!=p[j].c&&multi(p[j]-p[i],p[k]-p[i])>0)
num[p[k].c]=num[p[k].c]+1.0;
for(int k=1;k<=m;k++)
f*=num[k]/tot[k];
ans+=0.5*f*multi(p[i],p[j]);
}
printf("%.8lf",ans);
return 0;
}