考的河北 (13) 年的省选题。
实际得分:(15+100+20 = 135)
(T1) 博弈论的题,读懂题目就花了半个小时。
(T2) 之前做过的一道题,不说了。
(T3) 一道推柿子的题,不知道为什么链的暴力分挂掉了。
T1 Eden的博弈树
题目描述
给你一棵博弈树,叶子节点的状态是任意的,定义最小黑方胜集合为选最少的叶子节点使他们的状态为黑方必胜态,且根节点的状态为黑方必胜。最小白色胜集合为选最少的叶子节点使他们的状态为白方必胜,且根节点为白方必胜。问你既属于最小黑方胜集合,又属于最小白方胜集合的叶子节点的编号最小值,个数以及编号的异或和。
数据范围:(nleq 2 imes 10^5)
solution
树形 (dp) 加博弈论。
设 (f[i][0/1]) 表示 (i) 号点为白方/黑方必胜态的最少需要叶子结点的个数。
分情况来讨论转移:
- (i) 号点为黑方操作,(i) 号点为黑方必胜态只需要满足儿子节点中有一个为黑方必胜态,即
f[i][1] = min(f[i][1],f[to][1])
- (i) 号点为白方操作,则 (i) 号点为黑方必胜态就需要满足儿子节点都为黑方必胜态,即
f[i][1] += f[to][1]
- (i) 号点为黑方操作,则 (i) 号点为白方必胜态就需要满足儿子节点都为白方必胜态,即
f[i][0] += f[to][0]
- (i) 号点为白方操作,则 (i) 号点为白方必胜态只需要满足儿子节点中有一个为白方必胜态,即
f[i][0] = min(f[i][0],f[to][0])
在求 (f[i][0/1]) 的同时,拿 (vector) 存下当前这个点是由谁转移过来的。
找属于最小黑方胜集合中的叶子节点时,我们从根节点开始搜索,每次往当前节点的转移节点 ((vector) 中的点)中搜索,所搜到的叶子节点就是属于最小黑方胜集合中的点。找最小白方胜集合中的点同理。
代码很好实现,就 (4) 遍 (DFS) 。
$ update: $ 其实不需要拿 (vector) 存由谁转移过来的,只需要判断 (f[i][0/1]) 和 (f[to][0/1]) 就可以找到转移节点。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 2e5+10;
int n,v,tot,ans1,ans2,ans3;
int head[N],siz[N],dep[N],f[N][2],num[N];
struct node
{
int to,net;
}e[N<<1];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
return s * w;
}
void add(int x,int y)
{
e[++tot].to = y;
e[tot].net = head[x];
head[x] = tot;
}
void dfs1(int x,int fa,int type)
{
dep[x] = dep[fa]+1; siz[x] = 1;
if((dep[x] & 1) == type) f[x][type] = n+1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa) continue;
dfs1(to,x,type);
siz[x] += siz[to];
if((dep[x] & 1) == type) f[x][type] = min(f[x][type],f[to][type]);
else f[x][type] += f[to][type];
}
if(siz[x] == 1) f[x][type] = 1;
}
void dfs2(int x,int fa,int type)
{
if(siz[x] == 1) num[x]++;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa) continue;
if((dep[x] & 1) != type) dfs2(to,x,type);
else if(f[x][type] == f[to][type]) dfs2(to,x,type);
}
}
int main()
{
// freopen("a.in","r",stdin);
// freopen("a.out","w",stdout);
n = read(); ans1 = n+1;
for(int i = 2; i <= n; i++)
{
v = read();
add(i,v); add(v,i);
}
dfs1(1,0,1); dfs1(1,0,0);
dfs2(1,0,1); dfs2(1,0,0);
for(int i = 1; i <= n; i++)
{
if(num[i] == 2)
{
ans1 = min(ans1,i);
ans2++;
ans3 ^= i;
}
}
printf("%d %d %d
",ans1,ans2,ans3);
fclose(stdin); fclose(stdout);
return 0;
}
T2 ALO
题意描述
给你一个序列,定义一个区间的价值为区间的次大值 (k) 和区间中所有元素异或的最大值。
问你价值最大的区间的价值为多少。
数据范围:(nleq 50000,a_ileq 10^9)
solution
我们可以求出每个元素作为次大值的区间,然后用可持久化 (tire) 树就可以求出当前元素和这一段区间的元素的异或最大值。
问题就转化为怎么求每个元素作为次大值的区间。
记 (head[x]) 为左边第一个比 (a[x]) 大的元素,(net[x]) 为右边第一个比 (a[x]) 大的元素。
那么 (a[x]) 可以作为次大值的区间为:(head[head[x]]+1sim net[x]-1) 和 (head[x]+1,net[net[x]]-1) 。
(head[x],net[x]) 可以拿链表维护出来。
然后这道题就做完了,复杂度 (O(nlog^2n))。
代码实现也比较简单,主要是一些边界问题要注意一下。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
int n,ans,tot;
int a[N],head[N],net[N],rt[N],siz[10000010],tr[10000010][2];
struct node
{
int pos,w;
}e[N];
inline int read()
{
int s = 0, w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
return s * w;
}
bool comp(node a,node b)
{
return a.w < b.w;
}
void insert(int p,int q,int x)
{
for(int i = 30; i >= 0; i--)
{
int c = (x>>i)&1;
siz[p] = siz[q] + 1;
tr[p][!c] = tr[q][!c];
tr[p][c] = ++tot;
p = tr[p][c];
q = tr[q][c];
}
siz[p] = siz[q] + 1;
}
int query(int p,int q,int x)
{
int res = 0;
for(int i = 30; i >= 0; i--)
{
int c = (x>>i)&1;
if(siz[tr[q][!c]]-siz[tr[p][!c]] > 0)
{
res += (1<<i);
p = tr[p][!c];
q = tr[q][!c];
}
else
{
p = tr[p][c];
q = tr[q][c];
}
}
return res;
}
int main()
{
n = read();
rt[0] = ++tot;
insert(rt[0],rt[0],0);
for(int i = 1; i <= n; i++)
{
a[i] = read();
e[i].w = a[i];
e[i].pos = i;
rt[i] = ++tot;
insert(rt[i],rt[i-1],a[i]);
}
for(int i = 1; i <= n+1; i++) head[i] = i-1, net[i] = i+1;
net[0]=1;
sort(e+1,e+n+1,comp);
for(int i = 1; i <= n; i++)
{
int x = e[i].pos;
if(head[x] != 0) ans = max(ans,query(rt[head[head[x]]],rt[net[x]-1],e[i].w));
if(net[x] != n+1) ans = max(ans,query(rt[head[x]],rt[min(n,net[net[x]]-1)],e[i].w));
net[head[x]] = net[x];
head[net[x]] = head[x];
head[x] = net[x] = -1;
}
printf("%d
",ans);
return 0;
}
T3 SAO
题意描述
Welcome to SAO ( Strange and Abnormal Online)。这是一个 VR MMORPG, 含有 (n) 个关卡。但是,挑战不同关卡的顺序是一个很大的问题。
有 (n – 1) 个对于挑战关卡的限制,诸如第 (i) 个关卡必须在第 (j) 个关卡前挑战, 或者完成了第 (k) 个关卡才能挑战第 (l) 个关卡。并且,如果不考虑限制的方向性, 那么在这 (n – 1) 个限制的情况下,任何两个关卡都存在某种程度的关联性。即, 我们不能把所有关卡分成两个非空且不相交的子集,使得这两个子集之间没有任何限制。
对于每个数据,输出一行一个整数,为攻克关卡的顺序方案个数,(mod 1,000,000,007) 输出。
简化题意:给你 (n-1) 个限制,限制包括 (i) 要排在 (j) 前面或者 (i) 要排在 (j) 之后。问你有多少种排列满足这 (n-1) 个限制。
数据范围:(Tleq 5,nleq 1000)。
solution
树形 (dp) 。
我们可以把这 (n-1) 个限制看成边,这样就构成了一棵树。
我们设 (f[i][j]) 表示在 (i) 和 (i) 的子树构成的排列中,(i) 排第 (j) 个的方案数。
转移的时候,每次把 (to) 所在的子树合并进来,即把 (f[to][j]) 算到 (f[i][k]) 里面,分情况讨论一下。
- (x) 要在 (to) 之后。
枚举 (to) 在 (to) 的子树中排名,则有:
(f[x][i+j] = displaystylesum_{k = 1}^{j} f[x][i] imes f[to][k] imes {i+j-1choose i-1} imes {siz[x]+siz[to]-i-jchoose siz[x]-i})
简单来说就是,如果合并完 (to) 这棵子树后 (x) 的排名为 (i+j), 那么就意味着要从 (to) 子树中选 (j) 个点排在 (x) 的前面。因为 (x) 要在 (to) 之后,所以 (to) 必然在选出来的 (j) 个点当中,即 (to) 在 (to) 的子树中的排名范围为 (1-j) 。
合并后 (x) 的前面有 (i+j-1) 个位置,我们要把之前排在 (x) 前面的 (i-1) 个节点按排名顺序放到这 (i+j-1) 个位置中,方案数即为 (i+j-1choose i-1) , 同理 (x) 的后面有 (siz[x]+siz[to]-i-j) 个位置,排在 (x) 之后的 (siz[x]-i) 的节点按排名顺序放入这些位置的方案数为 (siz[x]+siz[to]-i-jchoose siz[x]-i) 。
- (x) 在 (to) 之前。
还是枚举 (to) 在 (to) 子树中的排名。
(f[x][i+j] = displaystylesum_{k = j+1}^{siz[to]} f[x][i] imes f[to][k] imes {i+j-1choose i-1} imes {siz[x]+siz[to]-i-jchoose siz[x]-i})
转移柿子和上面的差不多,只不过是 (k) 的枚举范围变了,因为 (to) 要排在 (x) 之后,所以 (to) 在 (to) 的子树中的排名就要大于选出来的 (j) 个点,即 (to) 在 (to) 的子树中的排名范围为 (j+1sim siz[to])。
这么直接转移的复杂度是 (O(n^3)) 的。
但我们发现当你枚举 (k) 的时候,后面乘的那一坨组合数是一样的。
考虑维护 (pre[x][i] = displaystylesum_{j=0}^{i} f[x][j]) ,(suf[x][i] = displaystylesum_{j=i}^{siz[x]+1} f[x][j]) 。
转移的时候用 (pre[x][i]) 和 (suf[x][i]) 就可以省掉枚举 (k) 的时间。
复杂度 (O(n^2))
Code
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define int long long
const int p = 1e9+7;
const int N = 2010;
int T,n,u,v,tot,ans;
int head[N],siz[N],f[N][N],g[N],c[N][N],sum1[N][N],sum2[N][N];
struct node
{
int to,net,id;
}e[N<<1];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
return s * w;
}
void add(int x,int y,int id)
{
e[++tot].to = y;
e[tot].id = id;
e[tot].net = head[x];
head[x] = tot;
}
void dfs(int x,int fa)
{
siz[x] = 1; f[x][1] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa) continue;
dfs(to,x);
memset(g,0,sizeof(g));//新开一个g数组避免重复更新
if(e[i].id == 1)
{
for(int j = 0; j <= siz[x]; j++)
{
for(int k = 0; k <= siz[to]; k++)
{
g[j+k] = (g[j+k] + f[x][j] * c[j+k-1][j-1] % p * c[siz[x]+siz[to]-j-k][siz[x]-j] % p * sum1[to][k] % p) % p;
}
}
}
else
{
for(int j = 0; j <= siz[x]; j++)
{
for(int k = 0; k <= siz[to]; k++)
{
g[j+k] = (g[j+k] + f[x][j] * c[j+k-1][j-1] % p * c[siz[x]+siz[to]-j-k][siz[x]-j] % p * sum2[to][k+1] % p) % p;
}
}
}
siz[x] += siz[to];
for(int j = 0; j <= siz[x]; j++) f[x][j] = g[j];
}
sum1[x][0] = 0; sum2[x][n+1] = 0;
for(int i = 1; i <= siz[x]; i++) sum1[x][i] = (sum1[x][i-1] + f[x][i]) % p;
for(int i = siz[x]; i >= 1; i--) sum2[x][i] = (sum2[x][i+1] + f[x][i]) % p;
}
void qingkong()
{
ans = tot = 0;
memset(head,0,sizeof(head));
memset(siz,0,sizeof(siz));
memset(sum1,0,sizeof(sum1));
memset(sum2,0,sizeof(sum2));
memset(f,0,sizeof(f));
}
signed main()
{
T = read();
c[0][0] = 1;
for(int i = 1; i <= 2000; i++)
{
c[i][0] = 1;
for(int j = 1; j <= i; j++)
{
c[i][j] = (c[i-1][j-1] + c[i-1][j]) % p;
}
}
while(T--)
{
n = read(); qingkong(); char ch;
for(int i = 1; i <= n-1; i++)
{
u = read() + 1; cin>>ch; v = read() + 1;
if(ch == '<') add(u,v,0), add(v,u,1);
else add(v,u,0), add(u,v,1);
}
dfs(1,0);
for(int i = 1; i <= n; i++) ans = (ans + f[1][i]) % p;
printf("%lld
",ans);
}
return 0;
}