CodeCraft-20 (Div. 2)
这一场的B和C都很好玩。题目链接:https://codeforces.com/contest/1316
A - Grade Allocation
诚心诚意的送分题。
*B - String Modification
挺好玩的一个题目,找规律找了半天。
题意:给一个长度不超过5000的字符串s,要求选择一个k(1<=k<=n),使得字符串s经过一个变换后的字典序最小,假如有多个这样的k,选择最小的一个。这个变换是:对于所有的i∈[1,n-k+1],依次翻转区间[i,i+k-1],例如:
s="123456" k=2
"[21]3456"
"2[31]456"
"23[41]56"
"234[51]6"
"2345[61]"
s="123456" k=3
"[321]456"
"3[412]56"
"34[521]6"
"345[612]"
看到这里,就感觉像是一个轮换,但是仔细观察会发现最后一段是有可能反过来的,比如:
s="12345" k=3
"[321]45"
"3[412]5"
"34[521]"
果断猜测是和奇偶性有关。强行构造出所有的变换,然后sort一遍就完事了。k=x,开头就是s[x],这个是显然的,而且后面的字符一个一个一直跟着直到s[x]s[n],这个也是显然的,前面的s[1]s[x-1]的“连接状态”不会被改变,这个也是显然的。关键是前面的s[1]~s[x-1]一共经过了n-k+1次翻转,所以有可能是反序的。
*C - Primitive Primes
题意:给一个n个项的多项式,系数都是整数,且未知数项为[0,n-1]次,保证所有的n个系数的gcd为1。再同理给出另一个m个项的多项式。再给一个质数p,求他们两个的多项式乘积的系数中,第几次项的系数不是p(p是质数)的倍数,显然这是有解的,假如多解输出其中任意一解。
题解:n和m太大,跑不动FFT,但是好像有人还真能卡过去(1497ms)。当时比赛的时候在想为什么题目说“显然这是有解的”,而且也不懂为什么要求p是质数,以及为什么要保证系数的gcd的1。画出多项式乘积的系数的计算方法 \(C_k=\sum\limits_{i=0}^{k}A_iB_{k-i}\) 后,可以看出相邻的两项的系数之间貌似不存在什么转移关系。
一次偶然的直觉,想到会不会是两个多项式分别的第一个非p的倍数,假设其是x,y,则可以得到:
p p x ...
p p p y ...
也就是
p p x a b c ...
p p p y d e ...
ep+dp+xy+ap+bp+cp
不管a,b,c,d,e是不是p的倍数,交叉组合之后都被乘了一个p,而xy也不可能是p的倍数,因为p是一个质数。所以上面这个式子模p不为0。
那么他们的系数的gcd为1,意思就是保证了至少存在一个非p的倍数,否则他们都是p的倍数,gcd至少就是p。也就是说他们本身gcd是不是1倒不影响,只要能找到一个不是p的倍数的数就行了。
D - Nash Matrix
题意:给一个n,然后一个n*n(n<=1e3)的矩阵,每个格子的信息表示从其出发最终会停留在哪个点,-1表示永远循环。要求构造一个相同大小的矩阵其中每个格子都唯一标有'U','D','L','R'其中之一或者'X',X表示停留在此处。要求任何时候都不能走出棋盘。
题解:被样例的构造误导到了奇怪的地方,以为“无限走”就要不断经过起始格子,实际上可以走到某个“核心”处然后在核心处绕圈圈。所以就找出所有的'X'格子,然后从他们开始bfs就把一整块都标记完了,然后找任意一个-1格子,假如它是孤立的-1格子则无解,否则两个-1格子就可以组成一个“核心”,再从这个两个格子的“核心”开始多源bfs就把一整块也标记完了。这时假如还有没被标记的格子,说明它和它的终点格子(一定有终点格子)不连通。
假如-1是要求经过起始格子并无限循环,除了要求一个连通块大小为偶数之外,还要要求“黑格”和“白格”的数量相等(并不是所有的偶数大小的连通块都可以形成回路),然后应该就是从度数最小的“凸起”格子开始贪心,写一个巨复杂的优先队列,注意删除一个格子的时候是删除一对格子,要把附近的总共6个格子的度数全部减1。
int n;
int x[1005][1005];
int y[1005][1005];
int vis[1005][1005];
int deg[1005][1005];
char ans[1005][1005];
int dx[4] = {0, 0, 1, -1};
int dy[4] = {-1, 1, 0, 0};
char dans[4] = {'R', 'L', 'U', 'D'};
char adans[4] = {'L', 'R', 'D', 'U'};
queue<pii> Q;
void bfs1(int ux, int uy) {
vis[ux][uy] = 1;
ans[ux][uy] = 'X';
Q.push({ux, uy});
while(!Q.empty()) {
int vx = Q.front().first;
int vy = Q.front().second;
Q.pop();
for(int i = 0; i < 4; ++i) {
int tx = vx + dx[i];
int ty = vy + dy[i];
if(1 <= tx && tx <= n && 1 <= ty && ty <= n) {
if(x[tx][ty] == ux && y[tx][ty] == uy && vis[tx][ty] == 0) {
vis[tx][ty] = 1;
ans[tx][ty] = dans[i];
Q.push({tx, ty});
}
}
}
}
}
void bfs2(int ux, int uy) {
vis[ux][uy] = 1;
Q.push({ux, uy});
for(int i = 0; i < 4; ++i) {
int tx = ux + dx[i];
int ty = uy + dy[i];
if(1 <= tx && tx <= n && 1 <= ty && ty <= n) {
if(x[tx][ty] == -1 && y[tx][ty] == -1 && vis[tx][ty] == 0) {
vis[tx][ty] = 1;
ans[ux][uy] = adans[i];
ans[tx][ty] = dans[i];
Q.push({tx, ty});
break;
}
}
}
if(Q.size() == 1) {
puts("INVALID");
exit(0);
}
while(!Q.empty()) {
int vx = Q.front().first;
int vy = Q.front().second;
Q.pop();
for(int i = 0; i < 4; ++i) {
int tx = vx + dx[i];
int ty = vy + dy[i];
if(1 <= tx && tx <= n && 1 <= ty && ty <= n) {
if(x[tx][ty] == -1 && y[tx][ty] == -1 && vis[tx][ty] == 0) {
vis[tx][ty] = 1;
ans[tx][ty] = dans[i];
Q.push({tx, ty});
}
}
}
}
}
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
scanf("%d%d", &x[i][j], &y[i][j]);
ans[i][j] = '.';
}
}
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
if(vis[i][j] == 0 && x[i][j] == i && y[i][j] == j)
bfs1(i, j);
}
}
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
if(vis[i][j] == 0 && x[i][j] == -1 && y[i][j] == -1)
bfs2(i, j);
}
}
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
if(vis[i][j] == 0) {
puts("INVALID");
exit(0);
}
}
}
puts("VALID");
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j)
putchar(ans[i][j]);
putchar('\n');
}
}
E - Team Building
题意:从n(<=1e5)个人里面,选出恰好p(<=7)个球员和恰好k(p+k<=n)个观众。p个球员对应p种不同的位置,给出每个人作为每种位置的收益,以及作为观众的收益。求最大的收益。
题解:看见这个p的数据量,感觉像是状压,但是k范围很大,定状态为dp[i][j]表示前i个人位置的占用状态为j的时候,忽略已经选了观众的人数,导致不知道一共选了多少个观众。但是仔细想想这个问题其实很好解决,按作为观众的收益从大到小排序,然后dp到前p+k个人的选法。后面的人就只能成为球员,不能成为观众。
细节:其实不是简单区分i和p+k等变量的关系,而是要数一数当前占用的观众数量,也就是i个人减去状态j中1的数量,就是观众的数量,假如观众还没满则可以转移观众,否则一定不优。最后会发现过不了样例3,显然是初始化没有标记非法状态的原因。
int n, p, k;
struct Node {
int a, s[7];
bool operator<(const Node &nd)const {
return a > nd.a;
}
} nd[100005];
ll dp[2][1 << 7];
int count1(int x) {
return __builtin_popcount(x);
}
void test_case() {
scanf("%d%d%d", &n, &p, &k);
for(int i = 1; i <= n; ++i)
scanf("%d", &nd[i].a);
for(int i = 1; i <= n; ++i) {
for(int b = 0; b < p; ++b)
scanf("%d", &nd[i].s[b]);
}
sort(nd + 1, nd + 1 + n);
memset(dp, -LINF, sizeof(dp));
dp[0][0] = 0;
for(int i = 1; i <= n; ++i) {
int t = i & 1;
for(int j = 0; j < (1 << p); ++j) {
dp[t][j] = dp[1 - t][j];
if(i - count1(j) <= k)
dp[t][j] += nd[i].a;
}
for(int j = 0; j < (1 << p); ++j) {
for(int b = 0; b < p; ++b) {
if(j & (1 << b))
continue;
dp[t][j | (1 << b)] = max(dp[t][j | (1 << b)], dp[1 - t][j] + nd[i].s[b]);
}
}
/*cout << "nd[" << i << "]=" << nd[i].a;
for(int b = 0; b < p; ++b)
cout << "," << nd[i].s[b];
cout << endl;
for(int j = 0; j < (1 << p); ++j)
cout << "dp[" << i << "][" << bitset<7>(j) << "]=" << dp[t][j] << endl;
cout << endl;*/
}
printf("%lld\n", dp[n & 1][(1 << p) - 1]);
}