「ZROI 2018」提高组十连测

蒟蒻的蜕变,神犇出现,终将与 Au 有缘!

Day1

A. 「ZYB建围墙」

显然,如果房子数量恰好能填充满一整个正六边形,那么在这个正六边形外面围一圈肯定是最优的。

那么我们只要考虑:在正六边形的基础上如果要增加一个房子,我们只要多使用长度为 $1$ 的墙就可以将一条边向外扩展一层(相邻两边的长度均增加 $1$,扩展的边长度减少 $1$)。

再考虑向外扩展后一层后,能多围的房子数量。假设当前正六边形的边长为 $x$,那么扩展 $6$ 次(每次扩展不同的边),每次的增加量分别是 $x-1$,$x$,$x$,$x$,$x$,$x+1$(很显然,连续扩展 $6$ 次后正好又是下一个正六边形了)。

那么,我们只要不断尝试增加边长 $x$,当 $x$ 恰好小于 $n$ 时(下一个边长围成的面积大于 $n$),贪心选择扩展次数。当围成的面积等于 $n$ 时需要特判!

时间复杂度:$O(\sqrt{n})$

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <cstdio>
#define quit(a) return printf("%d\n",a),0
int main() {
int n;
scanf("%d",&n);
for(int i=1;;++i) {
if(3*i*(i-1)+1==n) quit(6*i);
if(3*(i+1)*i+1>n) {
int now=3*i*(i-1)+1;
int ans=6*i;
if(now+(i-1)>=n) quit(ans+1); else now+=i-1;
if(now+i>=n) quit(ans+2); else now+=i;
if(now+i>=n) quit(ans+3); else now+=i;
if(now+i>=n) quit(ans+4); else now+=i;
if(now+i>=n) quit(ans+5); else now+=i;
if(now+(i+1)>=n) quit(ans+6); else now+=i+1;
}
}
return 0;
}

B. 「ZYB和售货机」

我们首先考虑一个问题:物品是否一定能被选完?

对此,我们只考虑 $f_i\le i$ 的情况(原因会在正解中解释)。如果我们从 $i$ 向 $f_i$ 连一条有向边,这显然是一个由根是自环的树组成的森林。而且每棵树的边都是朝向根的方向的!

有向边 $(x,y)$ 有意义当且仅当物品 $x$ 存在且 $y$ 可以被选择。因为边都是朝上的,那么我们从根向子树考虑,发现对于每个节点,其儿子肯定能把它的物品取空(因为每个儿子向父亲取物品,对应的 $a[i]$ 不变)。因此 每一个物品都能被取完。严格地说,每个非叶子节点的物品都能被取完

如果取消 $f_i\le i$ 的限制,那么形成的有向图是若干 基环内向树(基环树且树上的边都向上)。

现在我们只要考虑这个环。如果存在环上的一个点 $x$,按下这个按钮获得的收益不如另一条连向 $f_x$ 的树边,那么我们就直接把这条环边断开。这样就化为了树的做法,统计一遍即可。

如果不存在这样的点,那么我们就找一个环边价值和树边价值的差最小的点,强制它不能选择环边,统计答案即可。

实际上,我们只要记录到每个节点的最大和次大的边。$\text{DFS}$ 过程中,默认接上最大边的值,维护最大边和次大边的差的最小值。找到环之后,我们显然要断掉环——使连向某个点的边改变(从默认的最大变成次大),此时只要减去维护的最小值即可。

时间复杂度:$O(n)$

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <cstdio>
#include <algorithm>

const int N=100005;
int n,idx,mn,f[N],c[N],d[N],w[N],a[N],mx[N],secmx[N],dfn[N];
long long ans;

void dfs(int x) {
if(dfn[x]==idx) {
ans-=mn;
return;
}
if(dfn[x]) return;
dfn[x]=idx;
if(mx[x]) {
ans+=1LL*w[mx[x]]*a[x];
mn=std::min(mn,w[mx[x]]-w[secmx[x]]);
if(mx[x]^x) dfs(mx[x]);
}
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d%d%d%d",&f[i],&c[i],&d[i],&a[i]);
for(int i=1;i<=n;++i) {
w[i]=d[f[i]]-c[i];
if(w[i]<0) continue;
if(w[i]>w[mx[f[i]]]) {
secmx[f[i]]=mx[f[i]];
mx[f[i]]=i;
} else if(w[i]>w[secmx[f[i]]]) {
secmx[f[i]]=i;
}
}
for(int i=1;i<=n;++i) if(!dfn[i]) mn=(1<<30),idx++,dfs(i);
printf("%lld\n",ans);
return 0;
}

C. 「ZYB玩字符串」

观察删除 $p$ 的过程,虽然一个完整的 $p$ 可能被截成若干段,但是对于夹在中间的每一段,一定是能被独立删去的。

我们用 $f[i][j]$ 表示区间 $[i,j]$ 是否合法。

  • 如果 $len_p\mid (j-i+1)$,那么 $f[i][j]$ 表示是否能用 $p$ 把这段区间删光。
  • 否则,$f[i][j]$ 表示多余的部分是否恰好为 $p$ 的前缀。

具体转移过程(考虑第 $j$ 个字符):

  • 如果它和之后的零碎字符拼成一段,那么
  • 如果对于后面零碎部分而言,$j$ 属于夹在中间的整块,那么

这样的转移过程,看似复杂度是 $O(|S|^4)$ 的,但是对每个 $p$,先判断字符集的合法性,加入一些可行性或最优性剪枝,复杂度是很不满的。

时间复杂度:非常不满的 $O(|S|^4)$

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <cstring>
#define FOR(i,a,b) for(int i=a;i<=b;++i)
using namespace std;

int T,n,f[205][205],sz[305],cnt[305];
string s,t;

bool check(int len) {
memset(cnt,0,sizeof(cnt));
FOR(i,1,len) ++cnt[(int)t[i]];
FOR(i,0,300) {
if(!sz[i]&&!cnt[i]) continue;
if((sz[i]&&!cnt[i])||sz[i]%cnt[i]) return 0;
}
return 1;
}
int dfs(int len,int l,int r) {
if(~f[l][r]) return f[l][r];
if(l>r) return f[l][r]=1;
for(int i=r-len;i>=l;i-=len) {
if(dfs(len,l,i)&&dfs(len,i+1,r)) return f[l][r]=1;
}
if(s[r]==t[(r-l)%len+1]) return f[l][r]=dfs(len,l,r-1);
return f[l][r]=0;
}

int main() {
ios::sync_with_stdio(0); cin.tie(0);
for(cin>>T;T;--T) {
cin>>s;
n=(int)s.size(); s=' '+s;
memset(sz,0,sizeof(sz));
FOR(i,1,n) ++sz[(int)s[i]];
string ans=s;
FOR(len,1,n) {
if(n%len||len>(int)ans.size()-1) continue;
FOR(i,1,n-len+1) {
t=' '+s.substr(i,len);
if(!check(len)) continue;
memset(f,-1,sizeof(f));
if(dfs(len,1,n)) {
if(t.size()<ans.size()) ans=t;
else if(t.size()==ans.size()) ans=min(ans,t);
}
}
}
ans.erase(ans.begin());
cout<<ans<<'\n';
}
return 0;
}

Day2


Day3

咕咕


Day4

咕咕咕


Day5

咕咕咕咕

0%