题目链接
设第i个字符串存储在ss[i][]中。本题最直观最朴素的做法是枚举两个字符串ss[i]和ss[j] (i+1≤j) ,再用KMP算法匹配这两个字符串。首先从大到小枚举j,若对某个ss[j]存在某个ss[i]不是其子串,则立即可以得到答案j。但是,这样做的时间复杂度太高,因此行不通。不难看出,之所以复杂度这么高,是因为对任意的ss[i]和ss[j],朴素算法都要对其进行匹配。这是无疑是一种浪费。实际上我们只需要匹配一次就够了。
我的做法是,先令所有的ss[i] (i≠n) 与ss[n]进行一次匹配,若出现一次匹配失败则答案就是n。若全部匹配成功则可以得到一个重要的结论:所有的ss[i] (i≠n) 都是ss[n]的子串。也就是说我们在往后的比较中可以用ss[i] (i≠n) 在ss[n]中的位置信息(用区间来表示)取代ss[i]的字符串内容来判断这个字符串是不是另一个字符串的子串。
例如ss[1] = “ab”, ss[2] = “bc”, ss[3] = “abc”。在ss[1],ss[2]同ss[3]的比较中发现,ss[1]和ss[2]都是ss[3]的子串。对应的位置信息为 [0,1] 和 [1,2] ,由于前者不包含在后者中,因此答案为2。
#include <cstdio>
#include <cstring>
const int maxn = 5e2 + 5, maxLen = 2e3 + 5;
char ss[maxn][maxLen];
int t, n, ans, next[maxLen], a[maxn][2];
void getNext(char* p) {
int lp = strlen(p);
next[0] = -1;
for(int j=0, k=-1; j < lp-1;) {
if(k == -1 || p[j] == p[k]) {
j++, k++;
if(p[j] != p[k]) next[j] = k;
else next[j] = next[k];
}
else k=next[k];
}
}
int kmp(char* s, char* p) {
int i = 0, j = 0;
int ls = strlen(s), lp = strlen(p);
for(; i < ls && j < lp;) {
if(j == -1 || s[i] == p[j]) i++, j++;
else j = next[j];
}
if(j == lp) return i - j;
else return -1;
}
int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%s", ss[i]);
}
int len = strlen(ss[n]);
bool exist = false;
// 判断n是否是答案
for(int i = 1; i < n; i++) {
getNext(ss[i]);
int res = kmp(ss[n], ss[i]);
if(res == -1) {
exist = true;
break;
}
// 记录位置信息
a[i][0] = res;
a[i][1] = res + strlen(ss[i]) - 1;
}
if(exist) {
printf("Case #%d: %d\n", kase, n);
continue;
}
ans = -1;
// 枚举两个字符串,比较它们的位置信息
for(int i = n - 1; i > 0; i--) {
for(int j = 1; j < i; j++) {
if(a[i][0] > a[j][0] || a[i][1] < a[j][1]) {
ans = i;
i = 0;
break;
}
}
}
printf("Case #%d: %d\n", kase, ans);
}
return 0;
}
根据贝祖定理(扩展欧几里德算法的理论基础),对于任意两个整数 a,b ,存在 x,y 使得 x×a+y×b=gcd(a,b) 。也即是说, a,b 的任意整系数线性组合的结果都是它们的最大公约数的整数倍。而在本题中本结论又可以描述为: a,b 经过加减运算而衍生出来的新数,都是 gcd(a,b) 的整数倍。这样,我们通过求 gcd(a,b) 可以知道既满足 m=k×gcd(a,b),(k=1,2,3,...) 又满足 (m≤n) 的 m 的个数 num ,根据 num的 奇偶性就能知道最后的胜者是谁了。
#include <bits/stdc++.h>
using namespace std;
int t, n, a, b, g, ub, times;
int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
printf("Case #%d: ", kase);
scanf("%d%d%d", &n, &a, &b);
g = __gcd(a, b);
ub = n - n % g;
times = ub / g - 2;
puts(times % 2 ? "Yuwgna" : "Iaka");
}
return 0;
}
首先,根据贝祖定理可知,第i只青蛙能够到达的石块的编号组成的数列应是以起点编号0为首项,以 gcd(ai,m) 为公差的等差数列。以样例中的第一组为例,第1只青蛙( ai=2 )能够到达的石块的编号为 0,2,4,6,8,10 。
那么这是不是意味着我们可以枚举每只青蛙,然后对每只青蛙对应的等差数列求和,然后再将所有的数列和相加就能得到正确答案了呢?这种思路是不对的,因为这样简单地相加是会有重复的。例如 ai=2 的青蛙和 ai=3 的青蛙都会经过编号为 6 的石块。
所以,接下来的问题是要解决重复。对以样例第一组为例,最直观的办法是额外减去 ai=6 的青蛙(虚拟的青蛙)跳过的编号数列和。但事实上,只考虑减的问题对于某些样例而言并不足够。例如这样的样例:
n=3,m=12
a1=2,a2=3,a3=4
对第一只青蛙我们只需要在最后的结果ans变量中加上第一只青蛙对应的编号数列和即可。然后考虑第二只青蛙,除了加上数列和以外,还要减少一只虚拟青蛙 a=6 的数列和来消除重复。最后考虑第三只青蛙,除了加上第三只青蛙对应的而编号数列和以外,还需要减去一只虚拟青蛙 a=4 的数列和。这样看上去似乎是功德圆满了。确实,这回没有重复的数被加到ans中了,但是实际上答案中多减少了一只虚拟青蛙 a=6 的数列和,这是我们在减去虚拟青蛙 a=6 时不小心减去的。问题似乎变得复杂了。
其实,这样重复加需要减来抵消,重复减又需要加来抵消的情况符合容斥原理的模型(想象求几何图形交的总面积)。实现方法是:记 gi=gcd(ai,m) ,找出对 m 而言可能的 gi 的情况,也就是将 m 的因子找出来,存在fac数组中,再用数组painted标记本题实际出现的 gi 所能影响的全部因子fac[j],赋值fac[j] = 1。然后遍历painted数组,先将对应的数列求和,然后考虑若 painted[i]>0 则将faci影响的所有fac[j](满足 fac[j]modfac[i]=0 )减去painted[i]的数值,表示facj代表的数列已经全部被加到答案中了。若 painted[i]<0 则将fac[i]影响的所有fac[j]加上painted[i]的数值,表示fac[j]代表的数列受fac[i]的减行为的影响,需要额外的加。
这个过程像极了求几何图形交的总面积的过程,因此我才将表示某部分是否已经算入ans的标记数组的名字命名为painted。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e4 + 10, maxFac = 1e3;
int t, n, m, a, g[maxn], painted[maxFac];
vector <int> fac;
ll ans;
ll sum(ll n, ll d) {
return n * (n-1) / 2 * d;
}
int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%d%d", &n, &m);
// 输入数据的同时预处理出g
for(int i = 0; i < n; i++) {
scanf("%d", &a);
g[i] = __gcd(a, m);
}
fac.clear();
// 枚举m的因数
for(int i = 1; i * i <= m; i++) {
if(m % i == 0) {
fac.push_back(i);
if(i * i != m) {
fac.push_back(m / i);
}
}
}
sort(fac.begin(), fac.end());
memset(painted, 0, sizeof(painted));
for(int i = 0; i < n; i++) {
for(int j = 0; j < fac.size(); j++) {
// 初始化painted数组
if(fac[j] % g[i] == 0) {
painted[j] = 1;
}
}
}
ans = 0;
for(int i = 0; i < fac.size(); i++) {
if(painted[i] != 0) {
// 将数列和加到最后结果中
ans += painted[i] * sum(m / fac[i], fac[i]);
// 维护painted数组
for(int j = i + 1; j < fac.size(); j++) {
if(fac[j] % fac[i] == 0) {
painted[j] -= painted[i];
}
}
}
}
printf("Case #%d: %I64d\n", kase, ans);
}
return 0;
}
分别以两只牛的起点为起点求两个最短路数组,然后枚举每个点就能找到最好的那个点。注意,如果确定要在某个点相会,一只牛先到了的话需要等另外一只牛。另外,图的每个完全子图都要建边的话复杂度将难以让人忍受所以对于每个完全子图,设置一个虚拟的结点,再从虚拟结点向子图中的所有结点建边,这样建边的复杂度就降低到了线性复杂度。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair <ll, int> p;
const int maxn = 1e5 + 5, maxm = 1e6 + 5;
const ll INF = 1e15 + 5;
bool vis[maxn + maxm];
int t, m, n, si, ti;
ll ans, maxTime[maxn];
vector <int> vec;
// 边的结构体
struct edge {
int to, dist;
edge() {}
edge(int to, int dist): to(to), dist(dist) {}
};
// dijkstra算法模板
struct dijkstra {
ll d[2][maxn+maxm];
vector <edge> G[maxn+maxm];
void init() {
for(int i = 1; i <= n + m; i++) {
G[i].clear();
}
for(int i = 0; i < 2; i++) {
for(int j = 1; j <= n + m; j++) {
d[i][j] = INF;
}
}
}
void addEdge(int u, int v, int w) {
G[u].push_back(edge(v, w));
}
bool dfs(int u, int t) {
vis[u] = true;
if(u == t) return true;
bool res = false;
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i].to;
if(!vis[v]) res |= dfs(v, t);
}
return res;
}
void SSSP(int dir, int s) {
priority_queue < p, vector<p>, greater<p> > pq;
pq.push(p(0, s));
d[dir][s] = 0;
while(!pq.empty()) {
p node = pq.top();
pq.pop();
int u = node.second;
ll dist = node.first;
if(d[dir][u] < dist) continue;
for(int i = 0; i < G[u].size(); i++) {
edge& e = G[u][i];
if(d[dir][e.to] > dist + e.dist) {
d[dir][e.to] = dist + e.dist;
pq.push(p(d[dir][e.to], e.to));
}
}
}
}
}o;
int main() {
scanf("%d", &t);
for(int kase = 1; kase <= t; kase++) {
scanf("%d%d", &n, &m);
o.init();
for(int i = 1; i <= m; i++) {
scanf("%d%d", &ti, &si);
for(int v; si--;) {
scanf("%d", &v);
// 结点向虚拟节点建边
o.addEdge(v, n + i, ti);
// 虚拟节点向结点建边
o.addEdge(n + i, v, ti);
}
}
printf("Case #%d: ", kase);
memset(vis, 0, sizeof(vis));
bool flag = o.dfs(1, n);
if(flag) {
// 求两个方向上的最短路数组
o.SSSP(0, 1);
o.SSSP(1, n);
// 求到每个结点的相会耗时
for(int i = 1; i <= n; i++) {
maxTime[i] = max(o.d[0][i], o.d[1][i]);
}
// 找出最佳方案
ans = *min_element(maxTime + 1, maxTime + n + 1);
printf("%I64d\n", ans / 2);
vec.clear();
for(int i = 1; i <= n; i++) {
if(ans == maxTime[i]) {
vec.push_back(i);
}
}
for(int i = 0; i < vec.size(); i++) {
printf("%d", vec[i]);
putchar(i == vec.size() - 1 ? '\n' : ' ');
}
}
else puts("Evil John");
}
return 0;
}
(其它题目略)