文章目录
Go
题面
描述
Mirko很快厌倦了Jetpack Joyride并开始在他的手机上玩神奇宝贝GO。这个游戏的玩点之一就是神奇宝贝的进化。
为了进化物种为
P
i
P_i
Pi的神奇宝贝,Mirko必须提供
K
i
K_i
Ki个 用于该物种神奇宝贝的糖果。在神奇宝贝进化后,他能拿回
2
2
2个糖果。神奇宝贝只能在为他们的物种准备的糖果的帮助下进化。
Mirko有
N
N
N种神奇宝贝,并且为物种
P
i
P_i
Pi的神奇宝贝提供
M
i
M_i
Mi个糖果,他想知道他可以进化多少次神奇宝贝。
他还想知道哪个神奇宝贝可以进化最多次 。 如果有多个这样的神奇宝贝 , 输出一个编号最小的神奇宝贝。换句话说,在输入数据中最早出现的那个。
输入
第一行输入包含整数
N
N
N(
1
≤
N
≤
70
1≤N≤70
1≤N≤70)。表示神奇宝贝种类的数量。接下来
2
N
2N
2N行包含
N
N
N组数据,其中包含:
●第
2
i
2i
2i行包含字符串
P
i
P_i
Pi,由最多
20
20
20个字组成。表示第
i
i
i种神奇宝贝的名称;
●第
2
i
+
1
2i+1
2i+1行包含整数
K
i
K_i
Ki(
12
≤
K
i
≤
400
12 ≤K_i ≤ 400
12≤Ki≤400)和
M
i
M_i
Mi(
1
≤
M
i
≤
1
0
4
1≤M_i ≤ 10^4
1≤Mi≤104),分别第
i
i
i种神奇宝贝的进化所需的糖果数量和 o Mirko 为这种神奇宝贝准备的糖果总数。
输出
第一行输出一个整数。表示Mirko可以进化的神奇宝贝的总次数。
第二行输出一个字符串。表示可以进化最多次的神奇宝贝的名称。
分数分布
对于32%的数据, N=3 。
对于所有数据,正确输出第一行可以得到50%的分数, 正确输出第二行可以
得到另外50%的分数。
题解
签到题。
可以将每次进化送的两个糖果理解为:
第一次升级需要
K
i
K_i
Ki个糖果,之后升级只需
K
i
−
2
K_i-2
Ki−2个糖果。
因此可得答案为
m
a
x
(
0
,
⌊
M
i
−
2
K
i
−
2
⌋
)
max(0,\lfloor \frac{M_i-2}{K_i-2} \rfloor)
max(0,⌊Ki−2Mi−2⌋)。
由于C++是向0取整,以此只需用int计算
M
i
−
2
K
i
−
2
\frac{M_i-2}{K_i-2}
Ki−2Mi−2即可。
复杂度
O
(
N
L
)
O(NL)
O(NL)
实现
//T1
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
#define MAXN 70
#define MAXL 20
int n,ans;
char name[MAXN+5][MAXL+5];
int K[MAXN+5],M[MAXN+5];
int mid;
int get_val(int id){return (M[id]-2)/(K[id]-2);}
int main()
{
//freopen("go.in","r",stdin);
//freopen("go.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s%d%d",name[i]+1,&K[i],&M[i]);
if(get_val(i)>get_val(mid))mid=i;
ans+=get_val(i);
}
printf("%d\n%s",ans,name[mid]+1);
}
Tavan
题面
描述
小Zeljko一直在阁楼里读他奶奶的旧信,并且发现了一个长度为
N
N
N的单词 。
不幸的是,由于溢出的墨水,他不知道单词的内容。他把看不清的
M
M
M个字母每个字母都用一个字符#
替换后,在一张纸上重写了这个词。他把那张纸递给了他的奶奶,对于每个看不清的字母,奶奶给了他
K
K
K个不同的可能 。 在那之后,Zeljko在笔记本中写下了所有可能的单词,并决定仔细查看他们的属性,以确定原始单词是什么。在看到笔记本上写下的单词后,他的奶奶意识到他们正在寻找的是按字典序排列的第
X
X
X个单词。Zeljko在他们学校学习字母表的那天生病了,所以他要求你帮助他确定原来的单词。
输入
第一行输入包含整数
N
N
N,
M
M
M,
K
K
K和
X
X
X(
1
≤
N
≤
500
1≤N≤500
1≤N≤500,
1
≤
M
≤
N
1≤M ≤N
1≤M≤N,
1
≤
K
≤
26
1≤K≤26
1≤K≤26,
1
≤
X
≤
1
0
9
1≤X≤10^9
1≤X≤109)。分别表示单词的长度,看不清的字母的数量,奶奶给出的字母的数量和原单词是字典序的第几个。
第二行输入包含一个长度为
N
N
N 的字符串,由小写英文字母和字符#
组成 。
表示Zeljko找到的单词,其中字符#
表示看不清的字母。
接下来
M
M
M行中的每一行包含一个长度为
K
K
K的字符串,由
K
K
K个不同的小写英文字母组成。第
2
+
i
2+i
2+i行的
K
K
K个字母表示第
i
i
i个看不清的字母的
K
K
K种可能。
保证
X
X
X总是小于等于能构造出的单词的总数。
输出
输出一个字符串。表示原本的单词。
分数分布
对于30%的数据,
M
=
1
M=1
M=1并且
K
=
3
K=3
K=3。
对于另外30%的数据,
M
=
1
M=1
M=1
题解
简单题。
首先注意到看的清的字母对字典序没有任何影响,直接原样输出即可。
将每个看不清的字母的
k
k
k种可能按字典序排好。显然字典序最小的一个串就是每个字母的第一种可能,之后每次求下一个字典序就让最后一个字母移向下一种可能,若已到达第
k
k
k种就让前一个字母移向移向下一种可能,自己回到第一种可能。
这实际上就是
k
k
k进制数进位的过程,因此直接将
X
X
X转换为
k
k
k进制就能确定现在每个字母取到那种可能了。
复杂度
O
(
m
k
+
n
)
O(mk+n)
O(mk+n)
实现
//T2
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 500
int n,m,k,x;
char s[MAXN+5];
char p[MAXN+5][26+5];
int main()
{
//freopen("tavan.in","r",stdin);
//freopen("tavan.out","w",stdout);
scanf("%d%d%d%d",&n,&m,&k,&x);
scanf("%s",s+1);
for(int i=1;i<=m;i++)
{
scanf("%s",p[i]);
sort(p[i],p[i]+k);
}x--;
for(int i=strlen(s+1),j=m;i>=1;i--)
if(s[i]=='#')
{
s[i]=p[j][x%k];
x/=k;
j--;
}
printf("%s",s+1);
}
Nizin
题目
描述
Do Geese See God?或者,Was It A Rat I Saw?没关系,这只是展示Mislav对回文的热爱的不必要的介绍。帮他解决以下任务!
设
A
A
A是
N
N
N个整数构成的数组。我们说
A
A
A是回文,如果
A
i
=
A
N
−
i
+
1
A_i=A_{N-i+1}
Ai=AN−i+1 对于每个
i
i
i都成立,其中
A
i
A_i
Ai表示数组
A
A
A的第
i
i
i个元素,并且数组中的第一个元素编号是
1
1
1。
Mislav可以通过以下方式修改数组:在一次操作中,他选择该数组的两个相邻元素并用它们的和替换它们。注意,每次操作后数组中元素的数量将减少
1
1
1。
Mislav想知道为了让数组变为回文的,他必须做出最少多少次操作。
输入
第一行输入包含整数
N
N
N(
1
≤
N
≤
1
0
6
1≤N≤ 10^6
1≤N≤106),表示数组中的元素个数。
第二行输入包含
N
N
N个用空格分隔的正整数,表示Mislav数组中的元素。数
组中的数字最多为
1
0
9
10^9
109。
输出
输出一个整数。表示Mislav要把数组变为回文所需的最少操作次数。
分数分布
对于30%的数据,
N
≤
10
N≤10
N≤10。
对于60%的数据,
N
≤
1000
N ≤ 1000
N≤1000。
题解
简单题。
考虑从两边往中间解决这个问题。
若
A
l
<
A
r
A_l<A_r
Al<Ar因为两边必须相等,显然位置
l
l
l必须操作一次。
若
A
l
>
A
r
A_l>A_r
Al>Ar,同理。
若
A
l
=
A
r
A_l=A_r
Al=Ar满足条件,转移到到区间
[
l
+
1
,
r
−
1
]
[l+1,r-1]
[l+1,r−1]。
复杂度:
O
(
n
)
O(n)
O(n)
实现
//T3
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 1000000
#define LL long long
int n,l,r,cnt;
LL a[MAXN+5];
int main()
{
//freopen("nizin.in","r",stdin);
//freopen("nizin.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
l=1,r=n;
while(l<r)
{
if(a[l]==a[r])l++,r--;
else if(a[l]<a[r])a[l+1]+=a[l],l++,cnt++;
else a[r-1]+=a[r],r--,cnt++;
}
printf("%d",cnt);
}
Prosjecni
题目
描述
Slavko很无聊,所以他把正整数填到
N
∗
N
N*N
N∗N的方阵中。
如果他填出来的方阵满足以下条件,他会特别高兴:
● 每行中的数字的平均值是一个位于同一行的整数。
● 每列中的数字的平均值是一个位于同一列的整数。
● 表中的所有数字都不同。
帮助Slavko找到一种会让他开心的填法。
输入
第一行输入包含整数N( 1 ≤ N ≤ 100 1≤N≤100 1≤N≤100)。表示方阵的行数和列数。
输出
输出
N
N
N行,每行输出
N
N
N个由空格分隔的整数。令第
i
i
i行中的第
j
j
j个数字对应于Slavko将在方阵的第
i
i
i行第
j
j
j列写下的值。
所有数字必须大于
0
0
0且小于
1
0
9
10^9
109。
如果有多个解决方案,则输出任意一个。
如果没有任何解决方案,则输出-1
。
分数分布
无特殊分数分布。
题解
吐血构造题。
首先
n
n
n为奇数的情况使显然的,按顺序输出即可。
现在考虑
n
n
n为偶数的情况。
考虑打
n
=
4
n=4
n=4情况表。
写好暴力代码后,你会发现符合条件的矩阵相当的多,而题目只要求我们需找一个合法的解。
我们可以自己再添加一些条件使得构造更加方便。
首先再前面的大部分解中,都满足第一行为
1 2 3 6
容易发现
6
=
n
∗
(
n
−
1
)
2
6=\frac{n*(n-1)}{2}
6=2n∗(n−1),我们显然可以规定第一行就为
1
,
2
,
3
,
.
.
.
.
,
n
−
1
,
n
∗
(
n
−
1
)
2
1,2,3,....,n-1,\frac{n*(n-1)}{2}
1,2,3,....,n−1,2n∗(n−1)。
可以证明平均数就为
n
−
1
n-1
n−1。
类比
n
n
n为奇数的情况,我们可以发现这个方阵的行和列都有一个固定的差。
显然
n
n
n为偶数时差值是无法固定的,但仅有最后一行与倒数第二行的差值不同,是可以实现的。
考虑暴力枚举差值
K
K
K,为了保证每个数都不同,规定
K
≥
n
∗
(
n
−
1
)
2
K≥\frac{n*(n-1)}{2}
K≥2n∗(n−1),再暴力枚举最后一行,得到一个合法的答案。
可以发现
K
=
6
K=6
K=6时就有一个合法的解
1 2 3 6
7 8 9 12
13 14 15 18
31 32 33 36
通过找规律可以得到最后一行就是倒数第二行乘以n减去整行的和。
这样我们就可以归纳出整个矩阵的构造方法了。
复杂度:
O
(
n
2
)
O(n^2)
O(n2)
然而实际上构造并没有这么简单,毕竟有很多奇奇怪怪的矩阵都有这些规律。
这里放一个最神奇的:
1 2 6 15
4 9 13 10
7 14 8 3
16 11 5 12
实现
//T4
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 1000000
#define LL long long
int n;
int p[105][105];
int main()
{
//freopen("prosjecni.in","r",stdin);
//freopen("prosjecni.out","w",stdout);
scanf("%d",&n);
if(n==2)printf("-1\n");
else if(n%2)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
printf("%d ",n*(i-1)+j);
printf("\n");
}
}
else
{
for(int i=1;i<n;i++)p[1][i]=i;
p[1][n]=n*(n-1)/2;
for(int i=2;i<n;i++)
for(int j=1;j<=n;j++)
p[i][j]=p[i-1][j]+n*(n-1)/2;
for(int j=1;j<=n;j++)
{
p[n][j]=p[n-1][j]*n;
for(int i=1;i<n;i++)
p[n][j]-=p[i][j];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
printf("%d ",p[i][j]);
printf("\n");
}
}
/*for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=1;j<=n;j++)
sum+=p[i][j];
printf("...%d\n",sum/n);
}
for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=1;j<=n;j++)
sum+=p[j][i];
printf("%d ",sum/n);
}*/
}
/*
4
1 2 3 6
5 10 11 14
7 16 9 4
15 12 13 8
1 2 6 15
4 9 13 10
7 14 8 3
16 11 5 12
*/
Zamjene
题目
描述
Dominik想象出了一系列正整数
P
1
P_1
P1,
P
2
P_2
P2 ,
.
.
.
...
...,
P
N
P_N
PN。
让我们将他们排序之后的版本称为
Q
1
,
Q
2
,
.
.
.
,
Q
N
Q_1,Q_2 ,...,Q_N
Q1,Q2,...,QN。
此外,他想象出了一个允许替换集合。如果一对
(
X
,
Y
)
(X,Y)
(X,Y) 是允许替换集合的成员,Dominik可以交换数组
P
P
P中位于
X
X
X和
Y
Y
Y处的数字。
Marin向Dominik进行了
T
T
T次查询,每个查询都属于以下类型之一:
- 把数组 P P P中位于 A A A和 B B B位置的两个数字交换。
- 将 ( A , B ) (A,B) (A,B)添加到允许替换集合中。Marin可能给出已经在允许替换集合中的数对 ( A , B ) (A,B) (A,B)。
- 看看是否可以仅使用允许替换集合中的替换进行排序?可以以任意顺序使用替换,并且可以使每个替换任意次数。
- 如果位于
A
A
A位置的数字可以仅使用允许的替换转移到位置
B
B
B, 那么我们称
A
A
A和
B
B
B是链接的。我们把所有和
A
A
A链接的位置构成的集合称为
A
A
A的云。如果对于一个云中的每个元素
j
j
j,都能在仅使用允许替换集合中的替换使得
P
j
=
Q
j
P_j=Q_j
Pj=Qj,那么我们称这个云是好的 。 你必须回答有多少对不同的满足以下条件的位置
(
A
,
B
)
(A,B)
(A,B):
i. 位置 A A A和 B B B不是链接的
ii. A A A和 B B B的云都不是好的
iii. 如果我们将对 ( A , B ) (A,B) (A,B)添加
到允许的替换集合中, A A A的云(通过使 A A A和 B B B成为链接的来得到)变为好的。
请注意:对 ( A , B ) (A,B) (A,B)和 ( B , A ) (B,A) (B,A)被认为是完全相同的一对位置。
输入
第一行输入包含整数
N
N
N和
M
M
M(
1
≤
N
,
M
≤
1
0
6
1≤N,M ≤ 10^6
1≤N,M≤106)。分别表示数组
P
P
P的大小和查询的次数。
第二行输入包含
N
N
N个整数
P
1
,
P
2
,
.
.
.
,
P
N
P_1,P_2 ,...,P_N
P1,P2,...,PN(
1
≤
P
i
≤
1
0
6
1≤P_i≤ 10^6
1≤Pi≤106)。表示数组
P
P
P。
接下来
M
M
M行中的每一行都包含以下格式的查询:
● 行中的第一个数字是查询的类型
T
T
T。类型有1,2,3,4
四种,对应题目中说的四种查询。
● 如果是查询类型
T
T
T是1
或2
,会跟着两个不同的整数
A
A
A和
B
B
B(
1
≤
A
,
B
≤
N
1≤A,B≤N
1≤A,B≤N) 。
表示前两种询问中的
(
A
,
B
)
(A,B)
(A,B)。
输出
对于每个类型
T
T
T为3
或4
的查询,在单独的一行中输出答案。
查询类型3
的答案是DA
(表示可以只通过替换排序)或NE
(表示不可以只通过替换排序),没有引号。
查询类型4
的查询答案是一个非负整数。表示满足条件的位置对的数量。
分数分布
对于50%的数据, N ≤ 1000 N≤1000 N≤1000且 M ≤ 1000 M≤1000 M≤1000 。
题解
有些恶心的优化数据结构题。
首先读懂题目就是一个大问题,现在来翻译一下
操作 | 要求 |
---|---|
1 | 交换两个位置上的元素 |
2 | 合并两朵云 |
3 | 所有的云是否都是好 的 |
4 | 计算有多少组坏 的云合在一起之后就成了好 的,权值是两个云大小的乘积 |
第4
个操作是相当难以解决的,试着先解决前3
个操作的情况。
通过操作2
,我们很容易想到用并查集。
并且该题没有位置的分离操作,不需要可持久化。
现在的关键问题就是如何在支持一朵云单点修改的同时快速判定一朵云的好坏。
我们知道只要一朵云内包含元素种类和个数与目标序列
Q
Q
Q中对应的云都相同,那么这朵云就是好的。
然而我们并不能直接开数组统计,毕竟时间和空间复杂度都无法承受。
考虑使用玄学的哈希来检验。
这里使用
c
n
t
n
P
n
+
.
.
.
+
c
n
t
2
P
2
+
c
n
t
1
P
+
c
n
t
0
cnt_nP^n+...+cnt_2P^2+cnt_1P+cnt_0
cntnPn+...+cnt2P2+cnt1P+cnt0统计(取
P
=
1000003
P=1000003
P=1000003)。
合并时将两个数直接相加即可,单点修改也类似。
考虑先预处理数列
Q
Q
Q的hash值,将其与云种
P
P
P的hash值比较,就可以判断这朵云的好坏了。每次修改时统计云好坏的个数,操作3
也完成了。
现在我们再来考虑操作4
。
设两朵云
A
A
A,
B
B
B。
设它们的
P
P
P值,
Q
Q
Q值分别为
P
A
P_A
PA,
Q
A
Q_A
QA,
P
B
P_B
PB,
Q
B
Q_B
QB。
如果要计入显然要满足条件
P
A
+
P
B
=
Q
A
+
Q
B
P_A+P_B=Q_A+Q_B
PA+PB=QA+QB且
P
A
≠
Q
A
,
P
B
≠
Q
B
P_A≠Q_A,P_B≠Q_B
PA̸=QA,PB̸=QB。
发现式子和
A
A
A,
B
B
B都有关,这样做显然是
O
(
n
2
)
O(n^2)
O(n2)的。
因此需要变换一下,得到
Q
A
−
P
A
=
P
B
−
Q
B
Q_A-P_A=P_B-Q_B
QA−PA=PB−QB,
P
A
−
Q
A
≠
0
,
P
B
−
Q
B
≠
0
P_A-Q_A≠0,P_B-Q_B≠0
PA−QA̸=0,PB−QB̸=0。
我们只需要统计
Q
−
P
Q-P
Q−P互为相反数,且
Q
−
P
Q-P
Q−P不为零的对的个数就可以了。
这显然可以用map进行维护。[下标为
Q
−
P
Q-P
Q−P,值为云的大小的总和]
每次进行1
,2
操作时,我们只需要动态维护一下
Q
−
P
Q-P
Q−P的值,同时增加或删除对数的个数即可。
这样,整道题就顺利地解决了。
复杂度
O
(
n
l
o
g
n
+
n
α
(
n
)
)
O(nlogn+nα(n))
O(nlogn+nα(n))(这里假设
n
n
n,
m
m
m同阶)
常数相当大。
实现
//T5
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
#include<cstring>
#include<map>
using namespace std;
#define LL long long
#define MAXN 1000000
#define HASH 1000003
int n,Q,p[MAXN+5],q[MAXN+5],bad[MAXN+5],fa[MAXN+5],siz[MAXN+5];
LL pows[MAXN+5],nid[MAXN+5],gol[MAXN+5],cpairs;
map<LL,int> DIC;
void add_dic(int vd,LL val,int pn)
{
if(val!=0)cpairs+=vd*pn*DIC[-val];
DIC[val]+=vd*pn;
}
void add_hash(int vd,int u,int pos,int val)
{
gol[u]+=vd*pows[pos];nid[u]+=vd*pows[val];
siz[u]+=vd;
}
int xfind(int x){
return fa[x]=(x==fa[x])?x:xfind(fa[x]);
}
void merge(int x,int y)
{
x=xfind(x),y=xfind(y);
if(x==y)return ;
add_dic(-1,gol[x]-nid[x],siz[x]);
add_dic(-1,gol[y]-nid[y],siz[y]);
fa[y]=x;
gol[x]+=gol[y],nid[x]+=nid[y],siz[x]+=siz[y];
add_dic(1,gol[x]-nid[x],siz[x]);
}
int main()
{
//freopen("zamjene.in","r",stdin);
//freopen("zamjene.out","w",stdout);
scanf("%d%d",&n,&Q);
pows[0]=1;
for(int i=1;i<=MAXN;i++)
pows[i]=pows[i-1]*HASH;
for(int i=1;i<=n;i++)
{scanf("%d",&p[i]);q[i]=p[i];fa[i]=i;siz[i]=1;}
sort(q+1,q+n+1);
for(int i=1;i<=n;i++){
gol[i]=pows[q[i]];nid[i]=pows[p[i]];
add_dic(1,gol[i]-nid[i],siz[i]);
}
while(Q--)
{
int kdf,a,b;
scanf("%d",&kdf);
if(kdf==1){
scanf("%d%d",&a,&b);
int u=xfind(a),v=xfind(b);
if(u==v){swap(p[a],p[b]);continue;}
add_dic(-1,gol[u]-nid[u],siz[u]);
add_dic(-1,gol[v]-nid[v],siz[v]);
add_hash(-1,u,q[a],p[a]);add_hash(-1,v,q[b],p[b]);
swap(p[a],p[b]);
add_hash(1,u,q[a],p[a]);add_hash(1,v,q[b],p[b]);
add_dic(1,gol[u]-nid[u],siz[u]);
add_dic(1,gol[v]-nid[v],siz[v]);
}
if(kdf==2){
scanf("%d%d",&a,&b);
merge(a,b);
}
if(kdf==3)printf((DIC[0]==n)?"DA\n":"NE\n");
if(kdf==4)printf("%lld\n",cpairs);
}
}
Burza
题目
描述
Daniel厌倦了找工作,所以他决定去拜访他的朋友 Stjepan。 令人惊讶的是,当他进入Stjepan 的家时,他遇到了一棵有
N
N
N个节点的树,节点用
1
1
1到
N
N
N的数字表示。1
号节点有一枚硬币。
Stjepan 告诉他 : “ 戴上这个眼罩 , 我们开始玩游戏吧 ! ”l Daniel 给了他一个奇怪的表情,但决定按他说的做。然后Stjepan告诉他游戏规则:
●Daniel选择一个节点并标记它
●Stjepan将硬币移动到一个与硬币当前所在的节点相邻的未被标记的节点
●Stjepan标记硬币刚刚离开的那个节点
这三个步骤重复,直到Stjepan再也无法移动。由于被蒙了住眼睛,Daniel并不清楚某个时刻硬币在哪个节点上。然而,他很清楚树的轮廓以及硬币在游戏开始时的位置。
Daniel突然意识到他在过去的两个小时里没有申请过一份工作,所以他希望能够快速完成游戏。现在他想知道他是否能以这样一种方式进行比赛:无论Stjepan做出什么动作,比赛都在最多
k
k
k次动作前结束 。换句话说,Stjepan使硬币移动的次数少于
k
k
k次。
帮助Daniel确定他是否能按时完成游戏,然后回去将他的简历发送给他从未听说过的公司。
输入
第一行输入包含两个整数
N
N
N和
K
K
K(
1
≤
K
≤
N
≤
400
1≤K≤N≤400
1≤K≤N≤400)。分别表示树的节点个数和Daniel要求必须小于的移动次数。
接下来
N
−
1
N-1
N−1行每行包含两个整数
A
A
A和
B
B
B(
1
≤
A
,
B
≤
N
1≤A,B≤N
1≤A,B≤N)。表示
A
A
A号点和
B
B
B号点之间有一条无向边。保证给定的图形是树。
输出
如果Daniel能按时完成游戏,那么输出DA
。否则输出NE
。输出均不含括号。
分数分布
无特殊分数分布。
题解
一道神奇的题目。
首先我们需要处理一下这棵树,设一号节点深度为0,那么深度大于
k
k
k的所有点都可以删除,另外,我们规定所有深度为
k
k
k的节点为叶子节点。
可以注意到的是,在深度越高的地方标记就越优,随着硬币的移动,那么在除第0层的每一层,都有且只有一个标记。
接下来就是玄学操作了。
注意到
k
,
m
k,m
k,m同阶,而仅从
k
=
m
k=m
k=m来看,我们发现
k
k
k取一些值时总是必胜的。
我们可以考虑缩小
k
k
k的范围。
考虑以下两种极端情况:
显然,图2要比图1更为极端一些,虽然图1的节约了两个点,但第1层一旦被堵,就会多损失
k
−
1
k-1
k−1个节点。因此我们应该用图二来算极端情况。
此时的叶子节点数量为
⌊
n
−
1
k
⌋
\lfloor \frac{n-1}{k} \rfloor
⌊kn−1⌋,当
⌊
n
−
1
k
⌋
≤
k
\lfloor \frac{n-1}{k} \rfloor≤k
⌊kn−1⌋≤k时,就肯定为必胜策略。
可得
⌊
n
−
1
k
⌋
≤
n
k
≤
k
\lfloor \frac{n-1}{k} \rfloor≤\frac{n}{k}≤k
⌊kn−1⌋≤kn≤k从而得出
n
≤
k
2
n≤k^2
n≤k2
因此
k
k
k的范围立即缩小为了
k
<
20
k<20
k<20。
因为每一层都有且只有一个标记,深度小于20,这明显是可以用状压dp解决的问题。
由于状压允许深度是无序的,我们就可以规定叶子的dp是有序的。
因此设计
d
p
[
i
]
[
S
]
dp[i][S]
dp[i][S]表示前
i
i
i个叶子是否可以通过堵住
S
S
S中每个数值标记为1的对应深度上的其中一个节点来实现。
设深度d可以堵住的区间集合为
{
(
L
d
,
1
,
R
d
,
1
]
,
(
L
d
,
2
,
R
d
,
2
]
,
.
.
.
,
(
L
d
,
p
,
R
d
,
p
]
}
\{ (L_{d,1},R_{d,1}],(L_{d,2},R_{d,2}],...,(L_{d,p},R_{d,p}]\}
{(Ld,1,Rd,1],(Ld,2,Rd,2],...,(Ld,p,Rd,p]}(其中仅能任选其一堵住)
我们将
L
d
,
k
L_{d,k}
Ld,k作为下标将编号插入vector中。
那么dp转移式就可以写为:
d
p
[
R
d
,
k
]
[
S
∣
1
<
<
(
d
−
1
)
]
∣
=
d
p
[
i
]
[
S
]
(
i
=
L
d
,
k
)
dp[R_{d,k}][S|1<<(d-1)]|=dp[i][S](i=L_{d,k})
dp[Rd,k][S∣1<<(d−1)]∣=dp[i][S](i=Ld,k)
最后检查
d
p
[
c
t
]
[
S
]
dp[ct][S]
dp[ct][S](ct为叶子数量)是否有一个为1即可。
时间复杂度:
O
(
n
2
n
)
O(n2^{\sqrt n})
O(n2n)
实现
//T6
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<set>
using namespace std;
#define MAXN 400
#define MAXK 19
int n,k,u,v,dep[MAXN+5];
int Lid[MAXN+5],Rid[MAXN+5],ct;
char dp[MAXN+1][1<<MAXK];
vector<int> G[MAXN+5],P[MAXN+5];
void dfs(int x,int fa)
{
dep[x]=dep[fa]+1;
if(dep[x]==k+1){
Lid[x]=ct++;Rid[x]=ct;
return ;
}Lid[x]=ct;
for(int i=0;i<G[x].size();i++)
if(G[x][i]!=fa)dfs(G[x][i],x);
Rid[x]=ct;
}
int main()
{
//freopen("burza.in","r",stdin);
//freopen("burza.out","w",stdout);
scanf("%d%d",&n,&k);
if(k*k>=n){printf("DA\n");return 0;}
for(int i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
G[u].push_back(v);
G[v].push_back(u);
}
dep[0]=0;
dfs(1,0);
dp[0][0]=1;
for(int i=1;i<=n;i++)
if(dep[i]>1)P[Lid[i]].push_back(i);
for(int i=0;i<ct;i++)
for(int S=0;S<(1<<k);S++)
{
if(!dp[i][S])continue;
for(int j=0;j<P[i].size();j++)
if(!(S>>(dep[P[i][j]]-2) & 1))
dp[Rid[P[i][j]]][S|(1<<(dep[P[i][j]]-2))]=1;
}
for(int S=0;S<(1<<k);S++)if(dp[ct][S]){printf("DA\n");return 0;}
printf("NE");
}