ABC225 (Virtual Participation)
文章目录
A - Distinct Strings (5:18)
题意:给 3 个小写字母,问能组合成多少种字符串。
显然答案与有多少个字母相同有关。
- 全部相同时答案为 1
- 有两个相同时答案为 3
- 互不相同时答案为 6
暴力特判输出即可。
B - Star or Not (8:12)
题意:给出一个图,问其是否星星图(菊花图),星星图的定义为有一个结点直接连接其它所有结点。
根据定义,花蕊的性质就是度为 N − 1 N-1 N−1 ,检查所有结点中是否存在即可。
C - Calendar Validator (25:13)
题意:给一个 1 0 100 × 7 10^{100}\times 7 10100×7 的矩阵 A A A , A i , j = ( i − 1 ) × 7 + j A_{i,j}=(i-1)\times7+j Ai,j=(i−1)×7+j,再给一个矩阵 B B B ,问矩阵 B B B 在不旋转的情况下,是否是 A A A 的子矩阵。
可以先确定 B B B 中的一个元素,然后推得是 A A A 的子矩阵的 B ′ B' B′ ,然后对比 B B B 和 B ′ B' B′ 即可。
推出 B B B 的左上角可能在 A A A 的位置 ( i , j ) (i,j) (i,j) ,然后枚举 B B B 中的每一个元素,判断其是否与计算推理出来的值相同即可。
D - Play Train (44:03)
题意:有 N N N 辆玩具车,给出 Q Q Q 次操作和询问,一共有三种类型:
1 x y
令 x x x 的尾部与 y y y 的头部相连。保证 x ≠ y x\neq y x=y , x x x 的尾部和 y y y 的头部在此操作前没有与其它车相连,在此操作前 x x x 和 y y y 属于不同列车组。2 x y
令 x x x 的尾部与 y y y 的头部分离。保证 x ≠ y x\neq y x=y ,此操作前 x x x 的尾部与 y y y 的头部相连。3 x
从头至尾地输出 x x x 所在的列车组的所有玩具车编号。
模拟链表操作即可,对每个玩具车记录前驱 p r e ( x ) {\rm pre}(x) pre(x) 和后继 n x t ( x ) {\rm nxt}(x) nxt(x) 。输出的时候,从 x x x 开始一直访问前前驱,然后倒序输出,再一直访问后继顺序输出即可做到 O ( n ) O(n) O(n).
E - フ/7 (贪心)
题意:在一个平面上,给出若干个"7"形状的图案,问以最优方式删除一些"7",最多有多少个"7"可以在原点完全可视。"7"由两条线段 A B , B C AB,BC AB,BC 组成,其中 A ( x i − 1 , y i ) , B ( x i , y i ) , C ( x i , y i − 1 ) A(x_i - 1,y_i),B(x_i,y_i),C(x_i,y_i-1) A(xi−1,yi),B(xi,yi),C(xi,yi−1) .一个"7"在原点完全可视,当且仅当四边形 O A B C OABC OABC 不与任何其它图案"7"相交。
如图所示,红色的图案即为"7",并且可以知道,图案 1 不是完全可视的,因为其与原点组成的四边形与图案 2 相交。
我们可以尝试发掘两个图案互不遮挡的条件。
可以发现,一个图案将遮挡从原点到图案的两条边所在的两条射线所夹的区域,令靠近 y y y 轴的直线斜率为 k 1 k_1 k1 ,靠近 x x x 轴的直线斜率为 k 2 k_2 k2 ,于是对于两个图案 p , q p,q p,q 有:
- 两个图案互不遮挡当且仅当 k 1 p ⩽ k 2 q k_{1p}\leqslant k_{2q} k1p⩽k2q 或 k 1 q ⩽ k 2 p k_{1q}\leqslant k_{2p} k1q⩽k2p
所以我们可以用区间表达"7",一个图案"7"所占有的区间就是 [ k 2 , k 1 ] [k_2,k_1] [k2,k1] ,于是问题转化为求最多互不相交区间。
经典贪心,按右端点从小到大排序,每次先选右端点小的即可得到答案。
比较斜率的 Trick (高中解析几何基础) : k 1 = y 1 x 1 , k 2 = y 2 x 2 , k 1 < k 2 ⇔ y 1 x 2 < y 2 x 1 k_1 = \dfrac{y_1}{x_1},k_2=\dfrac{y_2}{x_2},k_1 < k_2 \Leftrightarrow y_1x_2<y_2x_1 k1=x1y1,k2=x2y2,k1<k2⇔y1x2<y2x1
F - String Cards (贪心 DP)
题意:给出 N N N 张卡牌,每张卡牌上面有一个字符串,要求从中选 K K K 个,并自由排列组合后,使得字典序最小的方案。字典序相同时长度小的字符串字典序更小。
官方题解很详细,这里搬运翻译一下,并稍作批注:
介绍 4 种错解,并举出它们的反例。
错解 1
将 ( S 1 , … , S N ) (S_1,\ldots ,S_N) (S1,…,SN) 排序,使得 S i ⩽ S i + 1 S_i\leqslant S_{i + 1} Si⩽Si+1.
将前 K K K 个字符串首尾相连作为答案.
反例:
2 2
b
ba
正确答案 bab
,程序输出 bba
.
显然没有考虑到字符串长度不一的条件。
错解 2 (考场上就想到这…)
将 ( S 1 , … , S N ) (S_1,\ldots ,S_N) (S1,…,SN) 排序,使得 S i + S i + 1 ⩽ S i + 1 + S i S_i+S_{i+1}\leqslant S_{i+1}+S_{i} Si+Si+1⩽Si+1+Si.
将前 K K K 个字符串首尾相连作为答案.
反例:
2 1
b
ba
正确答案 b
,程序输出 ba
.
没有考虑到作为结尾的字符串后面不需再加字符串。
错解 3
将 ( S 1 , … , S N ) (S_1,\ldots ,S_N) (S1,…,SN) 排序,使得 S i + S i + 1 ⩽ S i + 1 + S i S_i+S_{i+1}\leqslant S_{i+1}+S_{i} Si+Si+1⩽Si+1+Si.
设 f ( i , j ) f(i,j) f(i,j) 表示在前 i i i 个字符串中选出 j j j 个首尾相连的最小字典序的字符串,
转移方程 f ( i , j ) = min { f ( i − 1 , j ) , f ( i − 1 , j − 1 ) + S i } f(i,j) =\min\{f(i-1,j),f(i-1,j-1)+S_i\} f(i,j)=min{f(i−1,j),f(i−1,j−1)+Si}
答案为 f ( N , K ) f(N,K) f(N,K).
反例:
3 2
baa
ba
b
正确答案 baab
,程序输出 baaba
.
同样是没有考虑到作为结尾的字符串后面不需再加字符,从前往后考虑总会使得正确答案是程序输出的前缀,于是在字符串长度的差异上,程序输出字典序总大于正确答案。
错解 4
将 ( S 1 , … , S N ) (S_1,\ldots ,S_N) (S1,…,SN) 排序,使得 S i + S i + 1 ⩽ S i + 1 + S i S_i+S_{i+1}\leqslant S_{i+1}+S_{i} Si+Si+1⩽Si+1+Si.
设 f ( i , j ) f(i,j) f(i,j) 表示在前 i i i 个字符串中选出 j j j 个首尾相连的最小字典序的字符串,
转移方程
f ( i , j ) = min { f ( i − 1 , j ) , min k < i { f ( i − 1 , j − 1 ) + S i } } f(i,j) =\min\{f(i-1,j),\min_{k<i}\{f(i-1,j-1)+S_i\}\} f(i,j)=min{f(i−1,j),k<imin{f(i−1,j−1)+Si}}
答案为 f ( N , K ) f(N,K) f(N,K).
反例:
4 3
bbaba
bba
b
b
正确答案 bbababb
,程序输出 bbababbab
.
还是老问题,转移过程中增加参与转移的状态并不能消除无法考虑到结尾字符串后面不需再加字符串的问题,还是会使输出成为答案的前缀。
正解
将 ( S 1 , … , S N ) (S_1,\ldots ,S_N) (S1,…,SN) 排序,使得 S i + S i + 1 ⩽ S i + 1 + S i S_i+S_{i+1}\leqslant S_{i+1}+S_{i} Si+Si+1⩽Si+1+Si . 这一步是必须的,因为它保证了这些字符串从前往后选或者从后往前选都一定能取得最佳方案,这样排序的原理就是贪心中的相邻交换法,我们发现错解 1 之所以错是因为令两个长度不一的字符串比较,但它们组合起来以后的情况与两个独立的字符串的情况完全不同,所以正解排序通过比较两个长度相同的字符串解决了这一问题。(原题解有详细证明)这一步排序提供了一种 DP 的顺序,消除了原问题的后效性,为 DP 做铺垫。
然后从上面的错解,我们知道,后面几种看似十分接近正解的解法,问题都出现在不能选择到最优的作为结尾的字符串,所以为什么不考虑先从后面开始选呢?设
f
(
i
,
j
)
f(i,j)
f(i,j) 表示在
(
S
i
,
…
,
S
N
)
(S_i,\ldots ,S_N)
(Si,…,SN) 中选出
j
j
j 个字符串首尾相连得到的最小字典序的字符串。那么对于枚举到一个新的字符串,我们有两种决策,一种是抛弃它,另一种是将其作为先前最优解的开头,于是有:
f
(
i
,
j
)
=
min
{
f
(
i
+
1
,
j
)
,
S
i
+
f
(
i
+
1
,
j
−
1
)
}
f(i,j) = \min\{f(i+1,j),S_i + f(i+1,j-1)\}
f(i,j)=min{f(i+1,j),Si+f(i+1,j−1)}
一些启示 :
- 字典序最小或最大的问题涉及贪心,可以运用常见的贪心方法辅助解题
- 选择字典序最小的字符串时,要从后往前选,避免程序输出成为答案的前缀
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
using namespace std;
int N,K,tot;
string s[55],f[55][55],inf;
string smin(string s1,string s2)
{
int len1 = s1.size();int len2 = s2.size();
for(int i = 0;i < min(len1,len2);i ++)
if(s1[i] != s2[i]) return (s1[i] < s2[i] ? s1 : s2);
return (len1 < len2 ? s1 : s2);
}
bool cmp(string s1,string s2)
{
string s3 = s1 + s2;
string s4 = s2 + s1;
int len = s3.size();
for(int i = 0;i < len;i ++)
if(s3[i] != s4[i]) return s3[i] < s4[i];
return s1.size() < s2.size();
}
int main()
{
scanf("%d%d",&N,&K);
for(int i = 1;i <= N;i ++) cin >> s[i],tot += s[i].size();
sort(s + 1,s + 1 + N,cmp);
for(int i = 1;i <= tot;i ++) inf += "z";
for(int i = 0;i <= N + 1;i ++)
for(int j = 1;j <= N;j ++)
f[i][j] = inf;
for(int i = N,ii = 1;i >= 1;i --,ii ++)
for(int j = 1;j <= min(ii,K);j ++)
{
f[i][j] = smin(f[i + 1][j],s[i] + f[i + 1][j - 1]);
// printf("f(%d,%d) : ",i,j);cout << f[i][j] << endl;
}
cout << f[1][K];
return 0;
}
G - X (网络流 最小割)
题意:给一个 H H H 行 W W W 列的网格,每一个正方形网格 ( i , j ) (i,j) (i,j) 权值为 A i , j A_{i,j} Ai,j ,现在想给若干个网格画上 X ,即连上两条对角线,其它网格不能画任何东西,问最大得分是多少。定义得分为画上 X 的网格的权值和减去一个常数 C C C 与最少笔画数的乘积,即 s c o r e = ∑ A i , j − C × n {\rm score} = \sum A_{i,j} - C\times n score=∑Ai,j−C×n.
翻译一下题解并稍作批注
官方题解通过题意转化与简化操作类型,将原问题转化为一个最小割问题。
首先要考虑用于画 X 的最小线段数怎么求。
- 对于网格 ( i , j ) (i,j) (i,j) ,当它的左上角 ( i − 1 , j − 1 ) (i-1,j-1) (i−1,j−1) 或右下角 ( i + 1 , j + 1 ) (i+1,j+1) (i+1,j+1) 有画了 X 的网格时,它可以少画一笔。
- 对于网格 ( i , j ) (i,j) (i,j) ,当它的左下角 ( i − 1 , j + 1 ) (i - 1,j + 1) (i−1,j+1) 或右上角 ( i + 1 , j − 1 ) (i + 1,j - 1) (i+1,j−1) 有花了 X 的网格时,它可以少画一笔。
然后可以将以上条件等价为 :
- 对于网格 ( i , j ) (i,j) (i,j) ,当它的左上角没有画了 X 的网格,那么它就要多画一笔。
- 对于网格 ( i , j ) (i,j) (i,j) ,当它的左下角没有画了 X 的网格,那么它就要多画一笔。
于是画 X 的最小线段数 n = n 1 + n 2 n=n_1+n_2 n=n1+n2 , n 1 n_1 n1 为满足以上第一种情况的网格数, n 2 n_2 n2 为满足以上第二种情况的网格数。
然后就将问题转化为:
对于每个网格,可以选择画或者不画,如果画了,就得到分数 A i , j A_{i,j} Ai,j ,不画就不得分。
同时,对于每一个画了的网格,如果左上角的网格没有画,就使答案减去 C C C ,如果右上角的网格没有画,也使答案减去 C C C .求最大得分。
求最大得分不好求,那就考虑求最小代价,将问题转化为:
对于每个网格,可以选择画或者不画,如果画了,就没有增加代价,如果不画,就增加代价 A i , j A_{i,j} Ai,j.
同时,对于每一个画了的网格,如果左上角的网格没有画,就增加代价 C C C ,如果右上角的网格没有画,也增加代价 C C C .求最小代价。
于是可以通过建立网络流模型,将上述问题转化为最小割问题。
考虑怎么建图。考虑我们的图是怎么样的,对于不画的格子,只能增加代价 A i , j A_{i,j} Ai,j ,并且不会有更多的代价增加,所以割断这一类边后,不需要再割这个格子的其它边。而对于画了的格子,代价为 A i , j A_{i,j} Ai,j 的边一定不被割掉,会有代价为 0 0 0 的边,还会有代价 C C C 的边,当其左上角和右上角的格子都画了 X 时,就会割掉代价为 0 0 0 的边,否则割掉一条或两条代价为 C C C 的边。
于是建立一个源点 S S S ,一个汇点 T T T ,按一下方案建图:
- 源点 S S S 向分别所有网格连一条代价为 A i , j A_{i,j} Ai,j 的边。
- 对于所有左上角没有网格的网格,分别向汇点连一条代价为 C C C 的边。
- 对于所有左下角没有网格的网格,分别向汇点连一条代价为 C C C 的边。
- 对于所有左上角有网格的网格,向汇点连一条代价为 0 0 0 的边,向左上角的网格连一条代价为 C C C 的边。
- 对于所有左下角有网格的网格,向汇点连一条代价为 $0 $ 的边,向左下角的网格连一条代价为 C C C 的边。
于是求出最小代价后,用总价值减去最小代价即为答案 A n s = ∑ A i , j − m i n c o s t {\rm Ans} = \sum A_{i,j} - {\rm mincost} Ans=∑Ai,j−mincost
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<queue>
using namespace std;
typedef long long ll;
const ll inf = 0xfffffffff;
ll A[105][105],C,sum;
int H,W,cnt = 1,tot,map[105][105];
int head[50005],p[50005],dep[50005];
struct edge{
int nxt;
int to;
ll cap;
}e[500005];
void addedge(int from,int to,ll val)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].cap = val;
head[from] = cnt;
cnt ++;
e[cnt].nxt = head[to];
e[cnt].to = from;
e[cnt].cap = 0;
head[to] = cnt;
return ;
}
bool bfs()
{
memset(dep,-1,sizeof(dep));
queue<int > q;int u = 0;
q.push(tot + 1);dep[tot + 1] = 1;
for(;!q.empty();)
{
u = q.front();
q.pop();
for(int i = head[u],v = 0;i;i = e[i].nxt)
{
v = e[i].to;
if(e[i].cap > 0 && dep[v] < 0)
{
q.push(v);
dep[v] = dep[u] + 1;
}
}
}
return dep[tot + 2] > 0;
}
ll dfs(int now,int t,ll flow)
{
if(now == t || flow == 0) return flow;
ll res = 0;
for(int &i = p[now];i;i = e[i].nxt)
{
int v = e[i].to;
if(e[i].cap > 0 && dep[v] > dep[now])
{
ll fl = dfs(v,t,min(flow,e[i].cap));
res += fl;
flow -= fl;
e[i].cap -= fl;
e[i ^ 1].cap += fl;
if(flow == 0) break;
}
}
return res;
}
void Dinic()
{
ll ans = 0;
while(bfs())
{
memcpy(p,head,sizeof(p));
ans += dfs(tot + 1,tot + 2,inf);
}
printf("%lld",sum - ans);
}
int main()
{
scanf("%d%d%lld",&H,&W,&C);
for(int i = 1;i <= H;i ++)
for(int j = 1; j<= W;j ++)
{
scanf("%lld",&A[i][j]);
tot ++;map[i][j] = tot;
sum += A[i][j];
}
for(int i = 1;i <= H;i ++)
{
for(int j = 1;j <= W;j ++)
{
addedge(tot + 1,map[i][j],A[i][j]);
if(i - 1 < 1 || j - 1 < 1) addedge(map[i][j],tot + 2,C);
else addedge(map[i][j],map[i - 1][j - 1],C),addedge(map[i][j],tot + 2,0);
if(i + 1 > H || j - 1 < 1) addedge(map[i][j],tot + 2,C);
else addedge(map[i][j],map[i + 1][j - 1],C),addedge(map[i][j],tot + 2,0);
}
}
Dinic();
return 0;
}