文章目录
Tarifa
题面
描述
Pero 已经与他的互联网提供商签了一个非常好的数据套餐。
Pero 每月可以使用
X
X
X兆流量。没有花完的流量,可以留到下个月再用。
当然,Pero只能花他实际拥有的流量。
如果我们知道Pero在使用该套餐的前
N
N
N个月中每个月花费了多少兆流量,
那么确定Pero 在第
N
+
1
N+1
N+1个月能使用多少兆流量。
输入
第一行输入包含整数
X
X
X (
1
≤
X
≤
100
1 ≤X ≤ 100
1≤X≤100 )。表示Pero每个月能使用的流量。
第二行输入包含整数
N
N
N (
1
≤
N
≤
100
1 ≤N ≤100
1≤N≤100 )。表示有多少个月。
接下来
N
N
N行中的每一行包含整数
P
i
P_i
Pi(
0
≤
P
i
≤
1
0
4
0 ≤P_i ≤ 10^4
0≤Pi≤104)。表示使用该计划的前N个月中每个月花费的流量。
保证任意时刻实际使用的流量不会超出 Pero 能使用的上限。
输出
一个整数。表示第 N + 1 N+1 N+1 个月 Pero 能使用多少流量。
分数分布
无特殊分数分布。
题解
签到题
输出
X
∗
(
N
+
1
)
−
∑
P
i
X*(N+1)-\sum P_i
X∗(N+1)−∑Pi
复杂度:
O
(
n
)
O(n)
O(n)
实现
//T1
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 100
int X,n,sum;
int p[MAXN+5];
int main()
{
//freopen("tarifa.in","r",stdin);
//freopen("tarifa.out","w",stdout);
scanf("%d%d",&X,&n);
for(int i=1;i<=n;i++)
{scanf("%d",&p[i]);sum+=p[i];}
printf("%d",X*(n+1)-sum);
}
Jetpack
题面
描述
Mirko得到了一部新手机作为他的生日礼物!像如今所有的孩子一样,他很快就下载了所有流行的手机游戏,包括Jetpack Joyride。
在游戏中,主角Barry穿过一个由
10
10
10行
N
N
N列相同大小的方格组成的场地。
最初, Barry 位于场地左下角的方格中心。Barry 一直以每秒一个方格的速度向右跑。此外,他必须避开障碍。
当Mirko 按下按钮时,Mirko打开他的超级专用喷气背包并开始以每秒一个方格的速度上升(仍然向右移动,现在以
45
°
45 °
45° 的角度向上倾斜移动直到他到达天花板,直到Mirko 释放屏幕)。当Mirko松开按钮时 ,Barry 开始以每秒一个方格的速度下降(现在再次沿
45
°
45 °
45° 的角度移动,但这次方向朝下,直到他到达
地板)。
Mirko最近才开始玩这个游戏,所以他并不是很擅长。他在YouTube上看到有人设法通过跨越所有
N
N
N列完成游戏,所以他要求你的帮助。他将为你提供游戏中场地的布局,你必须输出让他获胜的一组操作。
输入
第一行输入包含整数
N
N
N(
1
≤
N
≤
1
0
5
1 ≤N ≤ 10^5
1≤N≤105 )。表示场地有多少列。
接下来
10
10
10 行中的每一行包含
N
N
N个字符。字符是.
或者X
,表示游戏中场地的布局,字符X
表示障碍物, .
表示可以走的地方。
输出
第一行输出必须包含整数
P
P
P(
0
≤
P
≤
5
∗
1
0
4
0 ≤P ≤ 5*10^4
0≤P≤5∗104 )。表示Mirko按下按钮的次数 。
接下来
P
P
P 行,每行输出一次操作,以达到通关游戏的目的。
每次操作由两个整数
t
i
t_i
ti和
x
i
x_i
xi 决定,其中
t
i
t_i
ti表示本次操作按下按钮的开始时间,
x
i
x_i
xi表示本次操作按下按钮的持续时间。
所有操作必须按时间顺序进行排序。换句话说
t
i
+
x
i
≤
t
i
+
1
t_i+x_i≤t_{i+1}
ti+xi≤ti+1。此外,在游戏结束后不应该有操作开始,即
t
i
<
N
t_i <N
ti<N。
如果存在多个解决方案,则输出任意一个。
保证存在解决方案。
分数分布
无特殊分数分布。
题解
无聊又恶心的模拟。
这里使用dp实现。
设
d
p
i
,
j
dp_{i,j}
dpi,j为Mirko当前是否可以到达位置
(
i
,
j
)
(i,j)
(i,j)。
转移如下
d
p
i
,
j
=
d
p
i
,
j
−
1
∣
d
p
i
+
1
,
j
−
1
(
i
=
1
)
dp_{i,j}=dp_{i,j-1}|dp_{i+1,j-1}(i=1)
dpi,j=dpi,j−1∣dpi+1,j−1(i=1)
d
p
i
,
j
=
d
p
i
+
1
,
j
−
1
∣
d
p
i
−
1
,
j
−
1
(
2
≤
i
≤
9
)
dp_{i,j}=dp_{i+1,j-1}|dp_{i-1,j-1}(2≤i≤9)
dpi,j=dpi+1,j−1∣dpi−1,j−1(2≤i≤9)
d
p
i
,
j
=
d
p
i
,
j
−
1
∣
d
p
i
−
1
,
j
−
1
(
i
=
10
)
dp_{i,j}=dp_{i,j-1}|dp_{i-1,j-1}(i=10)
dpi,j=dpi,j−1∣dpi−1,j−1(i=10)
输出方案按照题意模拟即可…
[然而有三个月没写过代码的我还是写炸了…]
复杂度:
O
(
n
2
)
O(n^2)
O(n2)
实现
//T2
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 100000
int n,cnt;
char mp[11][MAXN+5];
int dp[11][MAXN+5];
int vis[MAXN+5];
void print(int x,int y)
{
if(x==10&&dp[x][y-1])print(x,y-1);
else if(x==10&&dp[x-1][y-1])print(x-1,y-1);
else if(x==1&&dp[x][y-1]){vis[y-1]=1;print(x,y-1);}
else if(x==1&&dp[x+1][y-1]){vis[y-1]=1;print(x+1,y-1);}
else if(dp[x-1][y-1])print(x-1,y-1);
else if(dp[x+1][y-1]){vis[y-1]=1;print(x+1,y-1);}
else return ;
}
int main()
{
//freopen("jetpack.in","r",stdin);
//freopen("jetpack.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=10;i++)
scanf("%s",mp[i]+1);
for(int i=1;i<=10;i++)
for(int j=1;j<=n;j++)
dp[i][j]=0;
if(mp[10][1]!='X')dp[10][1]=1;
for(int j=2;j<=n;j++)
for(int i=1;i<=10;i++)
if(mp[i][j]!='X')
{
if(i==1)dp[i][j]|=dp[i][j-1],dp[i][j]|=dp[i+1][j-1];
else if(i==10)
dp[i][j]|=dp[i][j-1],dp[i][j]|=dp[i-1][j-1];
else dp[i][j]|=dp[i-1][j-1],dp[i][j]|=dp[i+1][j-1];
}
for(int i=1;i<=10;i++)
if(dp[i][n]){print(i,n);break;}
for(int i=1;i<=n;i++)
{
int p=i;
while(vis[p])p++;if(vis[i])cnt++;i=p;}
printf("%d\n",cnt);
for(int i=1;i<=n;i++)
{
int p=i;
while(vis[p])p++;
if(vis[i])printf("%d %d\n",i-1,p-i);
i=p;
}
}
Cezar
题面
描述
Mirko有一组
N
N
N个不同的单词,他想用替换密码加密。
我们通过首先选择一个密钥(一种
26
26
26 个英语字母的排列)来加密使用替换密码的文本 。 然后我们用密钥的第一个字母替换所有出现的字母 a
, 所有出现的字母 b
都替换为密钥的第二个字母,依此类推,直到字母 z
。
除了这些词之外, Mirko 还有一个数组
A
A
A ,它由
1
1
1 到
N
N
N 的数字组成,按照一定的顺序给出 ( 换句话说,数组
A
A
A是从
1
1
1到
N
N
N的一个排列)。Mirko想要选择一个密钥,使加密和并按照字典序排序后的单词数组对应于数组
A
A
A 。更准确地说,他希望最初位于
A
i
A_i
Ai位置的单词在加密并按字典序排序后位于
i
i
i位置。
让我们回想一下,字典序是单词出现在字典中的顺序 。 如果我们比较两个单词,从左到右,我们找到两个单词第一个字母不同的位置,并且基于此,我们确定哪个单词在字典上更小。如果单词
X
X
X是单词
Y
Y
Y 的前缀,则视为单词
X
X
X字典序小于单词
Y
Y
Y。
Mirko目前没有加密的心情,所以他恳请你为他做这件事。
输入
第一行输入包含整数
N
N
N(
2
≤
N
≤
100
2≤N≤100
2≤N≤100)。表示单词的个数。
接下来
N
N
N行每行包含一个单词,由最多
100
100
100个小写英文字母组成 。
这些单词是互不相同的。
最后一行包含
N
N
N个整数。表示数组
A
A
A 。
输出
在不存在解决方案的情况下,输出NE
。
否则,在第一行输出 DA
,在第二行输出由
26
26
26 个不同小写字母组成的字符串,即密钥。
如果存在多个解决方案,则输出任意一个。
分数分布
对于30%的数据,原单词只会由a
到f
这
6
6
6个字母组成。
题解
中档题。
考虑将字符串按加密后的字典序插入tire树中,如果遇到有节点分叉,说明前面插入的节点所代表的字母加密后在当前字母加密后的前面,这样就让前面字母向后面字母连一条边。
建好图后按照拓补序依次排字母即可。
复杂度
O
(
k
n
)
O(kn)
O(kn)(k为字母种数)
实现
//T3
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#include<vector>
using namespace std;
#define MAXN 200
int n,fd;
char s[MAXN+5][105],dic[27];
int id[MAXN+5],rnk[MAXN+5],tr[MAXN*MAXN+5][26],nd,du[27],T;
vector<int> G[27+1];
queue<int> Q;
void Insert(int x)
{
int p=0;
for(int i=1;i<=strlen(s[x]+1);i++)
{
if(!tr[p][s[x][i]-'a'])tr[p][s[x][i]-'a']=++nd;
for(int k=0;k<26;k++)
if(k!=s[x][i]-'a'&&tr[p][k])
{G[k].push_back(s[x][i]-'a');du[s[x][i]-'a']++;}
p=tr[p][s[x][i]-'a'];
}
for(int k=0;k<26;k++)
if(tr[p][k])fd=1;
}
int main()
{
//freopen("cezar.in","r",stdin);
//freopen("cezar.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%s",s[i]+1);
for(int i=1;i<=n;i++)
scanf("%d",&id[i]);
for(int i=1;i<=n;i++)
Insert(id[i]);
if(fd){printf("NE\n");return 0;}
for(int i=0;i<26;i++)
if(du[i]==0)Q.push(i);
while(!Q.empty())
{
int ps=Q.front();
Q.pop();
dic[ps]='a'+T;T++;
for(int i=0;i<G[ps].size();i++)
{
int nxt=G[ps][i];
du[nxt]--;
if(du[nxt]==0)Q.push(nxt);
}
}
for(int i=0;i<26;i++)
if(du[i]!=0){printf("NE\n");return 0;}
printf("DA\n");
printf("%s",dic);
}
Mag
题面
描述
您将获得一棵由无向边连接的树,树上每个节点都分配了一个魔力值
X
i
X_i
Xi 。
路径的魔力值被定义为该路径上节点的魔力值的乘积除以该路径上节点的数量。
例如,路径上有两个点,魔力值分别为
3
3
3和
5
5
5,那么这条路径的魔力值就为7.5(
3
∗
5
/
2
3*5/2
3∗5/2)。
在给定的树中,找到具有最小魔力值的路径并输出该路径的魔力值。
输入
第一行输入包含整数
N
N
N(
1
≤
N
≤
1
0
6
1≤N ≤10^6
1≤N≤106)。表示树中的节点数。
接下来
N
−
1
N-1
N−1行中的每一行包含两个整数,
A
i
A_i
Ai 和
B
i
B_i
Bi(
1
≤
A
i
,
B
i
≤
N
1≤A_i,B_i ≤N
1≤Ai,Bi≤N)。表示每条边连接的两个节点的编号。
接下来
N
N
N行中的每一行包含一个整数
X
i
X_i
Xi(
1
≤
X
i
≤
1
0
9
1≤X_i≤10^9
1≤Xi≤109)。表示第
i
i
i个节点的魔力值。
输出
以分数
P
⋅
Q
−
1
P·Q^{-1}
P⋅Q−1的形式输出具有最小魔力值的路径的魔力值(
P
P
P和
Q
Q
Q是互质的整数)。
保证
P
P
P和
Q
Q
Q小于
1
0
18
10^{18}
1018 。
分数分布
对于20%的数据,
N
≤
1000
N ≤1000
N≤1000。
对于另外30%的数据,不会有节点和超过
2
2
2
题解
一道比较有趣的题。
首先注意到如果求的是一条链的平均值的最小值,那么答案就是最小的点权。
那么平均值与本题的魔法值有什么区别。
实际上,当权值不为1时,真的没有什么差别,因为所有权值都大于1,乘法的增速比加法快得多,魔法值更不可能比原来的权值要小。
而当权值为1时,乘积保持1不变,这时就有可能比原来的魔法值要小。
然而这时就有了个大问题,如果在一个多数数字为1的链中,出现了几个其它数,这时是否比原来更优呢?
现在我们可以通过两个极端情况来证明这一点。
设2的个数为
x
x
x,则1的个数为
x
+
1
x+1
x+1。
这时选全部的魔法值为
2
x
2
x
+
1
\frac{2^x}{2x+1}
2x+12x,只选1的魔法值为1
这时我们可以发现
x
=
1
x=1
x=1时最小。
设两边1的的个数为
k
k
k[显然两边的1个数相等时最小]
这时整个模型的魔法值为
M
2
k
+
1
\frac{M}{2k+1}
2k+1M,一条1
链的最小值为
1
k
\frac{1}{k}
k1。
M
2
k
+
1
<
1
k
\frac{M}{2k+1}<\frac{1}{k}
2k+1M<k1
解得
M
<
1
k
+
2
M<\frac{1}{k}+2
M<k1+2
因此
M
M
M只能为
2
2
2。
综合以上两种极端情况,我们可以得出结论:
最小路径上,有一个权值为2
或者没有,其余权值全部为1
。
现在考虑如何找到路径,显然可以用
d
p
dp
dp来完成这个工作。
一条路径可以由树上的一条或两条链组成。
设
d
p
1
i
dp1_i
dp1i为从
i
i
i点向下延伸能够得到的最长的全1
链,
d
p
2
i
dp2_i
dp2i为从
i
i
i点向下延伸能够得到的最长的有一个为2
的链。
计算好后在节点上合并路径即可。
具体转移见代码。
复杂度:
O
(
n
)
O(n)
O(n)
实现
//T4
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define MAXN 2000000
#define DB double
#define LL long long
int n,u,v;
int ma,mb,val[MAXN+5];
vector<int> G[MAXN+5];
int dp1[MAXN+5],dp2[MAXN+5];
int gcd(int a,int b)
{
if(b==0)return a;
return gcd(b,a%b);
}
void upd(int x,int y)
{
if((DB)x/y<(DB)ma/mb)ma=x,mb=y;
}
void dfs(int x,int fa)
{
int n1=0,n2=0,n3=0;
for(int i=0;i<G[x].size();i++)
{
int nxt=G[x][i];
if(G[x][i]!=fa)
{
dfs(G[x][i],x);
if(val[x]==1){dp1[x]=max(dp1[x],dp1[nxt]+1);
dp2[x]=max(dp2[x],dp2[nxt]+1);}
if(val[x]==2){dp2[x]=max(dp2[x],dp1[nxt]+1);}
if(dp1[n1]<dp1[nxt]){n2=n1;n1=nxt;}
else if(dp1[n2]<dp1[nxt])n2=nxt;
if(dp2[n3]<dp2[nxt])n3=nxt;
}
}
if(val[x]==1)
{
if(n3==n1){upd(2,dp2[n3]+dp1[n2]+1);}
else {upd(2,dp2[n3]+dp1[n1]+1);}
upd(1,dp1[n1]+dp1[n2]+1);
dp1[x]=max(dp1[x],1);dp2[x]=max(dp2[x],1);
}
if(val[x]==2)
{upd(2,dp1[n1]+dp1[n2]+1);dp2[x]=max(dp2[x],1);}
}
int main()
{
//freopen("mag.in","r",stdin);
//freopen("mag.out","w",stdout);
scanf("%d",&n);
ma=1000000000,mb=1;
for(int i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
G[u].push_back(v);
G[v].push_back(u);
}
for(int i=1;i<=n;i++)
{
scanf("%d",&val[i]);
ma=min(ma,val[i]);
}
dfs(1,0);
int Gd=gcd(ma,mb);
printf("%d/%d",ma/Gd,mb/Gd);
}
Kralj
题面
描述
年轻的统治者Mirko宣称自己是矮人王。听到这个消息后,Slavko感觉受到了威胁,并很快宣布自己是精灵王!由于这片土地上不可能有两位国王,他们决定一劳永逸地解决权力争端。
Slavko 将与王国最强大的
N
N
N个精灵从
1
1
1到
N
N
N编号,去拜访Mirko的城堡。
在城堡大厅,
N
N
N个最强的矮人坐成一圈迎接他们,矮人按顺时针从
1
1
1到
N
N
N编号。
进入城堡后,Mirko给了Slavko的每一个精灵一个号码
A
i
A_i
Ai,这是他们将要对抗的矮人的编号。不幸的是,他没有确定每个精灵应该得到一个唯一的对手,很快就会发生一场可怕的战斗。
他们决定以下列方式解决问题:
●Slavko将按照他选择的顺序将精灵一个接一个地送到大厅。下一个精灵只有在他前面的精灵坐下之后才能进入大厅。
● 编号为
k
k
k的精灵将首先接近标记为
A
k
A_k
Ak的矮人。如果矮人旁边没有精灵,他会坐在那里。否则,他将继续顺时针地绕着矮人的圈子走,直到找到一个身边没有精灵的矮人。
现在,由此产生的
N
N
N对精灵和矮人参加了摔跤比赛,强者总是获胜。
Slavko为此次活动做好了充分的准备。他研究了所有的战士,并确定了每个战士的实力。现在他想按照顺序将精灵送到大厅,在他们坐下之后,他们将为他带来最大的胜利。
帮助他计算精灵可以达到的最高胜利次数!
输入
第一行输入包含整数
N
N
N(
1
≤
N
≤
5
∗
1
0
5
1≤N≤ 5*10^5
1≤N≤5∗105)表示矮人和精灵的数量。
第二行输入包含
N
N
N个整数
A
i
A_i
Ai(
1
≤
A
i
≤
N
1≤A_i≤N
1≤Ai≤N)。表示Mirko为第
i
i
i个精灵选择的对手。
第三行输入包含
N
N
N个整数
P
i
P_i
Pi(
1
≤
P
i
≤
1
0
9
1≤P_i ≤ 10^9
1≤Pi≤109)。表示第
i
i
i个矮人的实力。
第四行输入包含
N
N
N个整数
V
i
V_i
Vi(
1
≤
V
i
≤
1
0
9
1≤V_i≤10^9
1≤Vi≤109)。表示第
i
i
i个精灵的实力。
输入保证这
2
N
2N
2N个实力值互不相同。
输出
一个整数。表示精灵可以达到的最大胜利次数。
分数分布
对于40%的数据,Mirko 给每个精灵选择的对手都是 1 1 1号矮人,即 A i = 1 ( 1 ≤ i ≤ N ) A_i =1(1≤i ≤N) Ai=1(1≤i≤N)
题解
感觉这题比T4简单…
矮人围成了一个圈,这其实是一个很麻烦的限制条件,因为不管我们从那里开始,我们总会担心会有矮人从后方来到当前位置。
那是否有两个矮人之间没有精灵经过呢?
显然是有的,精灵顺时针走的条件是已经有精灵坐在相应位置上,我们假设这个坐在位置上的精灵本身就坐在这个位置上,而是现在这个精灵计划在这个坐在位置上的精灵原本计划坐在的位置,这样不断假设下去,我们就可以用一个精灵覆盖掉所有精灵走过的间隙,而一个精灵是不会转完整整一圈的。
官方给了一个数学的方法证明,它也可以帮助我们找到这个位置。
设区间
[
l
,
r
]
[l,r]
[l,r]中的精灵数为
S
r
−
S
l
−
1
S_r-S_{l-1}
Sr−Sl−1(S为精灵数的前缀和),如果
S
r
−
S
l
−
1
>
r
−
l
+
1
S_r-S_{l-1}>r-l+1
Sr−Sl−1>r−l+1,说明这段区间有精灵会走出去,转化一下,变为
S
r
−
r
>
S
l
−
1
−
(
l
−
1
)
S_r-r>S_{l-1}-(l-1)
Sr−r>Sl−1−(l−1)
设数组
c
i
=
S
i
−
i
c_i=S_i-i
ci=Si−i
显然只要找到一个位置
k
k
k,使得任意一个位置
i
i
i,有
c
i
>
c
k
c_i>c_k
ci>ck,就可以认为这一段没有精灵走过(可以证明环对结论没有影响),而这个位置就是
c
i
c_i
ci的最小值。
这样,我们就找到了可以将环变成链的位置。
接下来的操作就简单了,我们只需要从左往右将计划在这个位置进入的精灵放进set中(题目保证了实力各不相同),每次寻找比矮人实力值大且差值最小的精灵进行攻击,否则就拿实力最低的送命。
可以证明这个贪心是正确的。
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
实现
//T5
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<set>
using namespace std;
#define MAXN 1000000
int n;
int A[MAXN+5],P[MAXN+5],V[MAXN+5];
int sum[MAXN+5];
vector<int> G[MAXN+5];
set<int> S;
int main()
{
//freopen("kralj.in","r",stdin);
//freopen("kralj.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&A[i]);
for(int i=1;i<=n;i++)scanf("%d",&P[i]);
for(int i=1;i<=n;i++){
scanf("%d",&V[i]);
sum[A[i]]++;
G[A[i]].push_back(V[i]);
}
int mval=0,mid=n;
for(int i=1;i<=n;i++)
{
sum[i]+=sum[i-1];
if(sum[i]-i<mval){mval=sum[i]-i;mid=i;}
}
int st=mid%n+1,i=st,ans=0;
do
{
for(int j=0;j<G[i].size();j++){S.insert(G[i][j]);}
set<int>::iterator p=S.upper_bound(P[i]);
if(p==S.end())S.erase(S.begin());
else ans++,S.erase(p);
i=i%n+1;
}while(i!=st);
printf("%d",ans);
}
Vjestica
题面
描述
年轻的英雄,冒险家Matej,经过漫长而艰苦的旅程,到达了他的最终目的地——邪恶女巫Marija的家。
为了完成他的冒险,他必须解决女巫给他的最后一个难题。
为了开始解决她的谜题 , 我们的英雄需要熟悉称为前缀树(trie)的数据结构。
前缀树是一种数据结构,它以下列方式表示来自某个集合的单词的所有前缀:
● 树的每个边都用字母表中的字母表示。
● 树的根表示空前缀。
● 树中的所有其他节点表示非空前缀,其表示方式是每个节点表示通过连接写在从树的根到该节点的边上的字母(按此顺序)获得的前缀。
● 永远不会有来自单个节点的标有相同字母的两条边(这样我们可以最小化表示所有前缀所需的节点数)。
只有当Matej学习了什么是前缀树之后谜题才真正开始!
正如您可能已经猜到的那样,女巫的
N
N
N个单词由英文字母的小写字母组成 。
如果女巫想要知道该组词的前缀树的节点数,那么这个谜题就会非常简单,但她对此并不感兴趣 。 她希望知道在以任意方式对每个单词进行字母重新排列后 ,前缀树的最小节点数。
帮助Matej找到谜题的答案!
输入
第一行输入包含整数
N
(
1
≤
N
≤
16
)
N(1≤N ≤16)
N(1≤N≤16)。表示单词的个数。
接下来
N
N
N 行中的每一行包含由小写英语字母组成的单个单词。
所有单词的总长度将少于
1
0
6
10^6
106。
输出
一个整数。表示女巫的谜题的答案。
分数分布
无特殊分数分布。
题解
这道题并不难。
由于
n
n
n的范围非常的小,首先就可以考虑状压dp。
设
d
p
[
S
]
dp[S]
dp[S]为现在选了一个字符串的集合,计算最少的节点数。
然后我们可以肯定的是,只要是
S
S
S中字符串都有的字母,合并成公共节点一定是最优的。
因此我们可以先预处理每个串各个字母的个数,就可以用
O
(
26
n
)
O(26n)
O(26n)的复杂度找到lcp。
由于该题可以承受
O
(
3
n
)
O(3^n)
O(3n)复杂度,我们可以直接枚举子集,暴力讨论即可。
状态转移方程:
d
p
[
S
]
=
min
(
d
p
[
S
′
]
+
d
p
[
S
−
S
′
]
−
l
c
p
(
S
)
)
dp[S]=\min(dp[S']+dp[S-S']-lcp(S))
dp[S]=min(dp[S′]+dp[S−S′]−lcp(S))(其中
S
′
S'
S′为
S
S
S的子集)
枚举子集可以通过以下代码实现
for(int pS=(S-1)&S;pS>0;pS=(pS-1)&S)
复杂度: O ( 3 n ) O(3^n) O(3n)
实现
//T6
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<set>
#include<cstring>
using namespace std;
#define MAXN 16
#define MAXL 1000000
int n;
char s[MAXN][MAXL+5];
int dp[1<<MAXN],cnt[MAXN+1][26];
int LCP(int S)
{
int len=0;
memset(cnt[n],0x3f,sizeof(cnt[n]));
for(int i=0;i<n;i++)
if(S&(1<<i)){
for(int k=0;k<26;k++)
cnt[n][k]=min(cnt[n][k],cnt[i][k]);
}
for(int k=0;k<26;k++)len+=cnt[n][k];
return len;
}
int dfs(int S)
{
int &ret=dp[S];
if(ret!=-1)return ret;
int cl=LCP(S);
if((S&(-S))==S)return ret=cl;//表示只选中一个串时,返回长度(注意要打括号)
ret=MAXL*2;
for(int pS=(S-1)&S;pS>0;pS=(pS-1)&S)
ret=min(ret,dfs(pS)+dfs(S^pS)-cl);
return ret;
}
int main()
{
//freopen("vjestica.in","r",stdin);
//freopen("vjestica.out","w",stdout);
memset(dp,-1,sizeof(dp));
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%s",s[i]);
for(int j=0;s[i][j];j++)
cnt[i][s[i][j]-'a']++;
}
printf("%d",dfs((1<<n)-1)+1);
}