Codeforces Round #578 (Div. 2)
题目链接:https://codeforces.com/contest/1200
A - Hotelier
送分题。
B - Block Adventure
一条简单的dp,注意负数。
C - Round Corridor
简单数论,随便弄个gcd除一下。
D - White Lines
题意:给出一个边长不超过2000的黑白格矩阵,有一个橡皮擦工具,可以把以(i,j)为左上端点的一个边长为k的正方形全部变成白格,求用恰好一次橡皮擦工具,最多拥有多少个全白行和全白列。
题解:假如只看全白行,就把第i行的第一个黑格fi和最后一个黑格la找出来,那么当且仅当在[i-k+1,i]*[la-k+1,fi]中使用橡皮擦工具可以使这一行变成全白,用个差分暴力一下可以维护。同理可以算出全白列的情况,然后把两次差分的结果合并起来。
char g[2005][2005];
int dr[2005][2005];
int dc[2005][2005];
int ans[2005][2005];
void test_case() {
int n, k;
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i)
scanf("%s", g[i] + 1);
for(int i = 1; i <= n; ++i) {
int fi = 0, la = 0;
for(int j = 1; j <= n; ++j) {
if(g[i][j] == 'B') {
if(fi == 0)
fi = j;
la = j;
}
}
if(fi == 0) {
for(int j = 1; j <= n; ++j) {
++dr[j][1];
--dr[j][n + 1];
}
} else {
int L = max(1, la - k + 1), R = fi;
if(L <= R) {
for(int j = max(1, i - k + 1); j <= i; ++j) {
++dr[j][L];
--dr[j][R + 1];
}
}
}
}
for(int j = 1; j <= n; ++j) {
int fi = 0, la = 0;
for(int i = 1; i <= n; ++i) {
if(g[i][j] == 'B') {
if(fi == 0)
fi = i;
la = i;
}
}
if(fi == 0) {
for(int i = 1; i <= n; ++i) {
++dc[1][i];
--dc[n + 1][i];
}
} else {
int U = max(1, la - k + 1), D = fi;
if(U <= D) {
for(int i = max(1, j - k + 1); i <= j; ++i) {
++dc[U][i];
--dc[D + 1][i];
}
}
}
}
/*for(int i = 1; i <= n; ++i)
for(int j = 1; j <= n; ++j)
printf("%d%c", dr[i][j], " \n"[j == n]);
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= n; ++j)
printf("%d%c", dc[i][j], " \n"[j == n]);*/
for(int i = 1; i <= n; ++i) {
int cur = 0;
for(int j = 1; j <= n + 1; ++j) {
cur += dr[i][j];
ans[i][j] += cur;
}
}
for(int j = 1; j <= n; ++j) {
int cur = 0;
for(int i = 1; i <= n + 1; ++i) {
cur += dc[i][j];
ans[i][j] += cur;
}
}
int res = 0;
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j)
res = max(res, ans[i][j]);
}
printf("%d\n", res);
}
*E - Compress Words
题意:给n个字符串,要求把他们依次首尾连接,每次连接可以把最长的重叠的前后缀重叠在一起,求结果。
假算法:看起来可以二分每次重叠的长度,然后用哈希来验证,问题在于如何维护后缀哈希,更暴力就再用一个线段树。虽然看起来这个算法的复杂度是O(nlognlogn),但是仔细想一想并不是这样,首先线段树更新的时候,单点更新一个字符,最多更新n次,这部分复杂度是O(nlogn),然后在二分每次重叠的长度的时候,设新的字符串的长度是x,需要二分logx次,每次询问是logn,但是由于所有的x加起来才是n这么多,最坏的情况是一共添加n次长度为1的新字符串,复杂度也是O(nlogn),直观理解可以知道,新增的字符串平均长度越长,复杂度越低,因为log是个增长及其缓慢的函数。
上面这个算法假在:最大的匹配长度并不是满足单调性的东西。
题解:把上面的二分去掉,直接换成暴力。反正新增的每个字符最多被检测一次,也就是说,需要注意到匹配的过程和原串的长度并没有什么必然联系。再想想,好像连线段树也可以不要了。
const int MAXN = 1000000;
const int BASE = 2333;
int BASEPOW[MAXN + 5];
void Init() {
BASEPOW[0] = 1;
for(int i = 1; i <= MAXN; ++i)
BASEPOW[i] = 1ll * BASEPOW[i - 1] * BASE % MOD;
}
char ans[MAXN + 5];
char tmp[MAXN + 5];
int anslen, tmplen;
int ha1[MAXN + 5];
int ha2[MAXN + 5];
void test_case() {
Init();
int n;
scanf("%d", &n);
anslen = 0;
while(n--) {
scanf("%s", tmp + 1);
tmplen = strlen(tmp + 1);
int maxlen = 0, ceilen = min(anslen, tmplen);
for(int x = 1; x <= ceilen; ++x) {
ha1[x] = (1ll * ans[anslen - x + 1] * BASEPOW[x - 1] + ha1[x - 1]) % MOD;
ha2[x] = (1ll * ha2[x - 1] * BASE + tmp[x]) % MOD;
if(ha1[x] == ha2[x])
maxlen = x;
}
for(int i = 1 + maxlen; i <= tmplen; ++i)
ans[++anslen] = tmp[i];
}
puts(ans + 1);
}
启示:哈希真是个好办法。
深入理解KMP:其实这个匹配形式一开始确实觉得像KMP,但是不知道假如套KMP的话,谁是text串,谁是pattern串。事实上因为每次匹配的长度最大就是min(anslen,tmplen),截取ans串的这个长度的后缀放在后面,截取tmp串的这个长度的前缀放在前面,然后求一次前缀函数,得到的结果就是最长的重叠的真前后缀的长度。但是怎么保证结果不超过这个长度呢?
比如:"ababa"+"babab"
正确的答案应该是:"abab"
但求出的最长重叠串是:"abababab"
解决的办法是在中间加个'#'。
变成:"ababa"+"#"+"babab"
求出的最长重叠串是:"abab"
#include<bits/stdc++.h>
using namespace std;
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
const int INF = 1061109567;
const ll LINF = 4557430888798830399ll;
const int MOD = 1000000007;
/*---*/
const int MAXN = 1000000;
int pi[2 * MAXN + 5];
char s[2 * MAXN];
void GetPrefixFunction(char *s, int sl) {
pi[0] = 0, pi[1] = 0;
for(int i = 1, k = 0; i < sl; ++i) {
while(k && s[i] != s[k])
k = pi[k];
pi[i + 1] = (s[i] == s[k]) ? ++k : 0;
}
}
char ans[MAXN + 5];
char tmp[MAXN + 5];
int anslen, tmplen;
void test_case() {
int n;
scanf("%d", &n);
anslen = 0;
while(n--) {
scanf("%s", tmp + 1);
tmplen = strlen(tmp + 1);
int ceilen = min(anslen, tmplen), sl = 0;
for(int x = 1; x <= ceilen; ++x)
s[sl++] = tmp[x];
//s[sl++] = '#';
for(int x = 1; x <= ceilen; ++x)
s[sl++] = ans[anslen - ceilen + x];
GetPrefixFunction(s, sl);
int maxlen = min(ceilen, pi[sl]);
for(int i = 1 + maxlen; i <= tmplen; ++i)
ans[++anslen] = tmp[i];
}
puts(ans + 1);
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t = 1;
//scanf("%d", &t);
for(int i = 1; i <= t; ++i) {
//printf("Case #%d:", i);
test_case();
}
return 0;
}
*F - Graph Traveler
题目:有n(<=1000)个点,每个点有[1,10]条出边,且每个点有个权值,从某个点出发的时候会加上当前点的权值,然后根据权值mod当前点的出边数量,选择那条唯一的出边走出去。很显然无论从哪个点以什么初始权值出发,都会无限走下去。若干次询问,每次询问,回答某个起点以某个初始权值出发,无穷次经过的点的数量。
这个东西并不等价于问题“给一片1000*10个点的内向基环树森林,每个点记录当前的总权值,对每一个起点确定从它出发可以无穷次经过的点的数量。”,因为每个点的出边数量是不一定的,所以不能在某一个点模出边数量。遂看题解。
题解:虽然不能在某一个点模出边数量,但是因为[1,10]的lcm是2520,所以真正有用的权值是[0,2520),构造一片1000*2520个点的内向基环树森林,dp即可。需要注意很多个细节,比如转移的时候是要加上终点的权值,而不是起点的权值。最好写个小的LCM=12观察样例,然后写几个id和反id观察是不是搞错了。
基环树计算环的大小,本身是不需要SET的,但是这里要记录原本的节点的编号,再去重,假如用不排序去重的方法有可能导致复杂度错误。
const int LCM = 2520;
const int MAXN = 1000;
const int MAXNLCM = MAXN * LCM;
int K[MAXN + 5];
int G[MAXNLCM + 5];
int id(int i, int j) {
return (i - 1) * LCM + j + 1;
}
int id_i(int id) {
return (id - 1) / LCM + 1;
}
int id_j(int id) {
return (id - 1) % LCM;
}
int C[MAXNLCM + 5], cntC;
int DP[MAXNLCM + 5];
set<int> SET;
stack<int> STACK;
int inC;
int dfs(int u, int c) {
if(C[u]) {
if(C[u] == c) {
inC = u;
return 0;
}
return DP[u];
}
C[u] = c;
if(DP[u] = dfs(G[u], c))
return DP[u];
if(inC) {
SET.insert(id_i(u));
STACK.push(u);
if(u == inC) {
int siz = SET.size();
while(!STACK.empty()) {
int TOP = STACK.top();
STACK.pop();
DP[TOP] = siz;
}
inC = 0;
SET.clear();
return DP[u];
}
return 0;
}
exit(-1);
}
void test_case() {
int n;
scanf("%d", &n);
int e[10];
for(int i = 1, k; i <= n; ++i) {
scanf("%d", &k);
k = (k % LCM + LCM) % LCM;
K[i] = k;
}
for(int i = 1, m; i <= n; ++i) {
scanf("%d", &m);
for(int mi = 0; mi < m; ++mi)
scanf("%d", &e[mi]);
for(int j = 0; j < LCM; ++j) {
int vi = e[j % m];
int vj = (j + K[vi]) % LCM;
G[id(i, j)] = id(vi, vj);
}
}
cntC = 0;
for(int i = 1; i <= LCM * n; ++i) {
if(C[i] == 0)
dfs(i, ++cntC);
}
int q;
scanf("%d", &q);
while(q--) {
int i, j;
scanf("%d%d", &i, &j);
j = (j % LCM + LCM) % LCM;
j = (j + K[i]) % LCM;
printf("%d\n", DP[id(i, j)]);
}
}
总结:基环树复杂版?基环树计算环的大小的方法大概就是这样了,若遇到环入口或者dfs返回值非0或者遇到其他dfs的染色块,则说明已经退出环,则计算栈中的环的信息,或者直接继承DP值。其他情况就是非环入口的环节点,返回值为0。注意在基环树中开始dfs并不能只从入度为0的点开始,有可能整个基环树就是一个环。但是从任意一个点开始是可以的,因为假如在环中,则这个就是环入口,会把整个环染色。否则一定会进入某个环入口,然后把整个环染色,无论如何都会把这个点能到达的环染色。
假如换成vector排序去重,好像使用了更多空间,不过节省了时间,道理是显然的,因为set是最多拥有1000个数,而vector是非常浪费空间的。有个办法是记录一个虚拟的vector大小,当大小超过某个极限时把vector中的元素插进set,然后改用set统计,这样貌似还不如直接用set统计,反正复杂度是对的。
基环树是否可以写成一个非递归的版本?