2017-2018 ACM-ICPC, Asia Daejeon Regional Contest
ECFINAL PK赛。
C
签到题。
DAG上的dp。
D
签到题。
根据数据范围可知暴力即可。
F
哲学题。
简单递归,甚至没有前几天的牛客21小白赛的B麻烦。
补题
这套题在之前补过一部分,好像是这十套里补过的唯一一套。
H
如果 m m m串在 n n n串的 k k k位置,那么获胜的情况为 ∑ i = 1 m [ b [ i ] 战 胜 a [ k + i ] ] \sum_{i=1}^{m}[b[i]战胜a[k+i]] i=1∑m[b[i]战胜a[k+i]]
枚举你出的三种拳,将序列中对应的位置设为1,其余位置则设为0;相应的,在电脑的出拳序列中将会输的位置设为1,其余位置设为0,那么在该情况喜爱,获胜的种数即为 ∑ i = 1 m a [ i + k ] ∗ b [ i ] \sum_{i=1}^ma[i+k]*b[i] i=1∑ma[i+k]∗b[i]
将 b b b序列做转置得到 b ′ b' b′,那么上述表达式则转化为卷积形式
∑ i = 1 m a [ i + k ] ∗ b ′ [ m + 1 − i ] \sum_{i=1}^ma[i+k]*b'[m+1-i] i=1∑ma[i+k]∗b′[m+1−i]
做一次fft然后获取第 m + 1 + k m + 1 + k m+1+k项的系数即可得到在所有位置 k k k开始比赛时的获胜数.
所以对每一种出拳做一次fft,求最大值之和即可获取最终答案。
有一个地方要注意,题目说明这两个序列并不需要完全包含,所以上述的位置 k k k可以取遍 k = 1 , 2 , … , n . k=1,2,\dots,n. k=1,2,…,n. 相应的,序列数组 a , b a,b a,b也要扩大范围来空设一部分元素。
I
KMP算法。
正着弄不方便,首先我们先将原序列翻转,做一次kmp得到next数组。然后我们枚举循环数序列的终点,画图求解后容易知道循环节的长度就是 i − n e x t [ i ] . i-next[i]. i−next[i]. 不过具体的表达式还是要根据next数组的具体意义等等来决定。
这道题好像在xdoj上有,但是我当时不会做。
总之这道题还是一个很巧妙的KMP应用题,如果在画图的时候用循环节一段段的表示的话其实想到用KMP也不是特别困难的事情。
K
构造一段方向不变,长度可以改变的折线段,要求直线间两两不能相交。
水题。不过当时场上被I打蒙了,所以这个题看都没有看一眼…
两种构造办法都可以成功,一个是从上限 n n n进行构造,一个是从下限 1 1 1进行构造,记录当前图形的四个边界后采用螺线形进行扩散即可。
B
不算难的一道暴力搜索题。
因为发现哪怕没有重力的限制,最多的状态总数也只有 3 16 = 4 e 7 3^{16}=4e7 316=4e7,再加上我们有重力约束、起始点的约束可以有效的剪不少枝,所以时间是没有太大的限制的,下面要做的就是仔细地写好这个dfs模拟。
int start, xx, yy;
int a[4][4];
unordered_set<int> s;
int range(int x, itn y){
if(x >= 0 && y >= 0 && x < 4 && y < 4) return 1;
return 0;
}
int ok(int x, int y, int dx, itn dy){
int x1 = x + dx, x2 = x - dx, y1 = y + dy, y2 = y - dy;
if(range(x, y) && range(x1, y1) && range(x2, y2) && a[x][y] == a[x1][y1] && a[x1][y1] == a[x2][y2]) return 1;
return 0;
}
int check(int x, int y, int dx, int dy){
if(ok(x, y, dx, dy) || ok(x - dx, y - dy, dx, dy) || ok(x + dx, y + dy, dx, dy)) return 1;
return 0;
}
bool isok(int x, int y, int op){
if(a[x][y] == op && (check(x, y, 1, 0) || check(x, y, 0, 1) || check(x, y, 1, 1) || check(x, y, 1, -1)))
return 1;
return 0;
}
int has(){
int x, y;
int res = 0;
for(int i = 15; i >= 0; i--){
x = i / 4; y = i % 4;
res *= 3;
res += a[x][y];
}
return res;
}
void dfs(int x, int op){
int i;
for(i = 0; i < 4; i++) if(a[i][x] == 0) break;
if(i == 4) return ;
a[i][x] = op;
if(isok(i, x, op)){
if(op == 1){a[i][x] = 0; return ;}
else{
if(i == xx && x == yy){s.insert(has()); a[i][x] = 0; return;}
else{a[i][x] = 0; return ;}
}
}
if(i == xx && x == yy && op == 1){a[i][x] = 0; return ;}
for(int j = 0; j < 4; j++) dfs(j, 3 - op);
a[i][x] = 0;
}
int main(){
// Fast;
scanf("%d %d %d", &start, &xx, &yy);
start--; xx--; yy--;
dfs(start, 1);
printf("%d\n", (int)s.size());
return 0;
}
代码粗理不粗,感觉写的还是比较有条理的…
最开始写完后怎么调都还是结果不对,仔细研究(小数据对拍1 2 1)后发现我没有判断白子在其余位置获胜时的情况,所以结果总是大于正确结果。
对于这种要仔细分类讨论的情况,一定要注意条件的充要
转化!
E
结合之前暑训的一部分内容(2019暑训8月5号 网络流)和一个非常好的博客网络流基础,我复习了一下网络流算法的思想。
现在再看网络流相关有了更深刻的理解。之前并没有很深刻地理解以增广路
为核心的FF算法、EK算法和Dinic算法,刚刚温故知新后发现增广路的重要地位。其实求解最大流最朴素的办法就是尝试找一条路其流量均严格小于其流量,这样就可以对这条路扩流;不断尝试直到没有一条增广路后我们就得到了最大流。而EK算法就是避免找增广路时越找越远,所以用bfs进行分层,分完层再用dfs进行扩流;不过EK算法的扩流是针对某一条可行流的扩流,每次对一条可行流扩流都会进行一次bfs,bfs的分层结果没有充分被利用;而Dinic算法则在bfs之后进行多路扩流,将bfs的结果完全的利用起来,这样就完成了算法效率的提升。
回到本题,对于某一条边,如何才可以保证其被一颗MST包含?当然应该使得其余比他权值低的边都被删除,这就是一个最小割问题。
当求解某一条对应的花费时,开设一个新边集数组,用来记录生成的网络流的边。我们遍历所有原始边,如果其权值严格小于当前边的权值,那么就在网络图中连一条对应方向、权值为1的边,同时加上一条权值为0的反向边;网络图生成完后,只需要求解从该选取的边的两个端点作为源点和汇点的最大流,这就是必须要删去的边的个数。
因为采取star存图,所以组成无向边的一组有向边会在上述遍历过程中分别被添加一次,也就是总共会有4条”重叠的“边。不过在求解最大流的时候,只有一组边被用到,根据链式前向星的性质知即边i和边i ^ 1。
也因此,网络图的边应该开 m a x e < < 2 maxe << 2 maxe<<2,而原始边集只需要开 m a x e < < 1 maxe << 1 maxe<<1,TLE7就是因为网络图的大小没有开够。
AC代码
const int maxn = 1e2 + 10;
const int maxe = 5e2 + 10;
struct star{
int from, to, w, next;
};
int head[maxn], top = 0;
star edge[maxe << 1];
int first[maxn], ttt = 0;
star net[maxe << 2];
void add(int u, int v, int w, int *head, int &top, star *edge){
edge[top].to = v;
edge[top].from = u;
edge[top].w = w;
edge[top].next = head[u];
head[u] = top++;
}
void netInsert(int u, int v, int w){
add(u, v, w, first, ttt, net);
add(v, u, 0, first, ttt, net);
}
int dis[maxn];
queue<int> q;
int bfs(int s, int t, int *head, star *edge){
memset(dis, -1, sizeof dis);
while(!q.empty()) q.pop();
q.push(s); dis[s] = 0;
while(!q.empty()){
int temp = q.front(); q.pop();
for(int i = head[temp]; ~i; i = edge[i].next){
int to = edge[i].to;
if(dis[to] == -1 && edge[i].w != 0){
q.push(to);
dis[to] = dis[temp] + 1;
}
}
}
return dis[t] != -1;
}
itn dfs(int s, int t, int maxflow, int *head, star *edge){
if(s == t || maxflow == 0) return maxflow;
int ans = 0;
for(int i = head[s]; ~i; i = edge[i].next){
int to = edge[i].to;
if(dis[to] != dis[s] + 1 || edge[i].w == 0 || ans >= maxflow) continue;
int f = dfs(to, t, min(edge[i].w, maxflow - ans), head, edge);
edge[i].w -= f; edge[i ^ 1].w += f;
ans += f;
}
return ans;
}
int dinic(int s, int t){
int ans = 0;
while(bfs(s, t, first, net))
ans += dfs(s, t, 0x3f3f3f3f, first, net);
return ans;
}
int cal(int x){
memset(first, -1, sizeof first); ttt = 0;
for(int i = 0; i < top; i++) if(edge[i].w < edge[x].w)
netInsert(edge[i].from, edge[i].to, 1);
return dinic(edge[x].from, edge[x].to);
}
int main(){
// Fast;
memset(head, -1, sizeof head);
memset(first, -1, sizeof first);
int n, m; scanf("%d %d", &n, &m);
for(int i = 0; i < m; i++){
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
add(u, v, w, head, top, edge); add(v, u, w, head, top, edge);
}
itn ans = 0;
for(int i = 0; i < top; i += 2) ans += cal(i);
printf("%d\n", ans);
return 0;
}
G
也是一道场上连看都没看一眼的不算太难的题目。
如果没有理解错,应该就是之前因为CF某个题而学到的扫描线算法。
将所有的拐点按照x坐标排序,维护up和down的值,不断更新答案和个数即可。
尽管如此,这道题还是耗费了我好久好久的时间debug,最后还是对拍才知道自己为什么永久WA3了…
坑点
注意题目允许两种楼梯存在,当两种单调性不同的楼梯交错时一定不会产生闭合区域,这里需要分类讨论。 (sample2对应的情况)
最开始的时候疑惑在最后如果出现infinite的区域该怎么办,仔细读题后注意到约束词closed,那么那些无穷大的边界区域就不予考虑,因而我们对区域个数cnt进行计数的时候只好在出现 d o w n > = u p down >= up down>=up 后即出现闭合后才能加1。不过,我最开始只关注到了右无穷的边界问题,在循环外加上了一句闭合判断,并没有注意到左无穷的边界问题,从而导致了一屏幕的WA test3…在考虑左无穷区域的时候,只有当左边出现过闭合条件 d o w n > = u p down >=up down>=up 后才可以开始计数,不然这些区域都是左无穷的一部分。
尽管这道题初看下来还算比较简单,但是还是有一两个小地方要注意到并且考虑清楚的…
还是读题不够细致,考虑情况不够周到,太菜了…
代码
struct Node{
ll x, y;
int op;
};
const int maxn = 25e3 + 10;
itn n, m;
Node node[maxn << 2];
int top = 0;
ll up, down;
bool cmp(Node a, Node b){
if(a.x != b.x) return a.x < b.x;
return a.y < b.y;
}
void update(int i){
if(node[i].op) up = node[i].y;
else down = node[i].y;
}
int main(){
// Fast;
scanf("%d %d", &n, &m);
int x, y, flag = 1;
scanf("%lld", &down);
for(int i = 0; i < n; i++){
scanf("%d %d", &x, &y);
node[top].x = x; node[top].y = y; node[top].op = 0;
top++;
}
scanf("%lld", &up);
for(int i = 0; i < m; i++){
scanf("%d %d", &x, &y);
node[top].x = x; node[top].y = y; node[top].op = 1;
top++;
}
flag ^= (node[0].y - down > 0);
flag ^= (node[n].y - up > 0);
//特判单调性不同的情况。
if(!flag){
puts("0 0");
return 0;
}
sort(node, node + top, cmp);
int pre = -1, preflag = 0;
ll res = 0, ans = 0, cnt = 0;
for(int i = 0; i < top; i++){
//存在区域 && i != 0 && 并不是左无穷区域
if(up > down){
if(~pre && preflag) res += (node[i].x - pre) * (up - down);
}
else{
preflag = 1;
if(res) cnt++;
ans += res; res = 0;
}
update(i);
while(i + 1 < top && node[i].x == node[i + 1].x){
update(++i);
}
pre = (int)node[i].x;
}
if(down >= up){
if(res) cnt++;
ans += res;
}
printf("%lld %lld\n", cnt, ans);
return 0;
}
参考博客:
「备战PKUWC2018」2017-2018 ACM-ICPC, Asia Daejeon Regional Contest