这个题目更奇葩。据说是上一个题的加强版。题意是给定M个模式串,然后给定长度L,问不超过L的文本至少含有一个模式的情况的总种数。
还是用模式串建立Trie图,根据Trie图建立起路径长度为1的矩阵M。
总情况数目为26^1+26^2+…+26^L。不含模式串的情况总数为矩阵N = M^1+M^2+M^3+…+M^L的第一行之和。总情况数目减去不含模式串的情况就是答案。
这里用到了矩阵的一些算法,比如快速冥,还有快速冥求和。但是,我用了操作符重载,最悲剧的是重载后的操作符没有优先级,而我还当作有优先级的在用,所以悲剧了。。。一直样例都过不去。。。唉,最后才发现了这个问题。。。写了260行左右的代码,前面的一部分代码可以当作矩阵操作的模板了。。。Trie图的也不错,过几天估计得打印下来用了。。。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#include <stdio.h>
#include <string.h>
#include <queue>
#include <algorithm>
using namespace std;

typedef unsigned long long INT;
const int MAX_D = 26;
const int MAX_L = 10;
const int MAX_N = 10;
char szPat[MAX_L];

const int MAX_S = 31;
struct Matrix
{
int nSize;
INT nD[MAX_S][MAX_S];
Matrix(int nS)
{
Clear(nS);
}

Matrix operator = (const Matrix m)
{
nSize = m.nSize;
for (int i = 0; i < nSize; ++i)
{
for (int j = 0; j < nSize; ++j)
{
nD[i][j] = m.nD[i][j];
}
}
return *this;
}
void Clear(int nS)
{
nSize = nS;
memset(nD, 0, sizeof(nD));
}
void Unit()
{
for (int i = 0; i < nSize; ++i)
{
for (int j = 0; j < nSize; ++j)
{
nD[i][j] = (i == j ? 1 : 0);
}
}
}
};

Matrix operator+(const Matrix A, const Matrix B)
{
Matrix C(A.nSize);

for (int i = 0; i < A.nSize; ++i)
{
for (int j = 0; j < A.nSize; ++j)
{
C.nD[i][j] = A.nD[i][j] + B.nD[i][j];
}
}
return C;
}

Matrix operator*(const Matrix nA, const Matrix nB)
{
Matrix nC(nB.nSize);
for (int i = 0; i < nA.nSize; ++i)
{
for (int j = 0; j < nA.nSize; ++j)
{
for (int k = 0; k < nA.nSize; ++k)
{
nC.nD[i][j] += nA.nD[i][k] * nB.nD[k][j];
}
}
}
return nC;
}

Matrix operator^(Matrix B, INT nExp)
{
Matrix ans(B.nSize);

ans.Unit();
while (nExp)
{
if (nExp % 2)
{
ans = ans * B;
}
B = B * B;
nExp >>= 1;
}
return ans;
}

//求base^1+base^2++base^N
Matrix SumPowMatrix(Matrix base, INT nN)
{
if (nN == 1)
{
return base;
}

Matrix ans = SumPowMatrix(base, nN / 2);
ans = ans + ((base^(nN / 2)) * ans);//重载运算符保证不了优先级
if (nN % 2)
{
ans = ans + (base^nN);//没优先级啊,必须加括号,查错2个小时了
}
return ans;
}

struct Trie
{
Trie* next[MAX_D];
Trie* fail;
int no;
bool flag;
};
Trie tries[MAX_L * MAX_N];
int nP;
Trie* pRoot;

Trie* NewNode()
{
memset(tries[nP], 0, sizeof(Trie));
tries[nP].no = nP;
return tries[nP++];
}

void InitTrie(Trie* pRoot)
{
nP = 0;
pRoot = NewNode();
}

void Insert(Trie* pRoot, char* pszPat)
{
Trie* pNode = pRoot;
while (*pszPat)
{
int idx = *pszPat - 'a';
if (pNode->next[idx] == NULL)
{
pNode->next[idx] = NewNode();
}
pNode = pNode->next[idx];
++pszPat;
}
pNode->flag = true;
}

void BuildAC(Trie* pRoot, Matrix M)
{
pRoot->fail = NULL;
queue<Trie*> qt;
qt.push(pRoot);

M.Clear(nP);
while (!qt.empty())
{
Trie* front = qt.front();
qt.pop();
for (int i = 0; i < MAX_D; ++i)
{
if (front->next[i])
{
Trie* pNode = front->fail;
while (pNode && pNode->next[i] == NULL)
{
pNode = pNode->fail;
}
front->next[i]->fail = pNode? pNode->next[i] : pRoot;
if (front->next[i]->fail->flag)
{
front->next[i]->flag = true;
}
qt.push(front->next[i]);
}
else
{
front->next[i] = front == pRoot? pRoot : front->fail->next[i];
}

//这里必须要加上front->flag为false的判断么?加不加会生成不同的矩阵
if (!front->next[i]->flag)
{
++M.nD[front->no][front->next[i]->no];
}
}
}
}

int main()
{
int nN;
INT nL;
Matrix M(0);

while (scanf("%d%I64u", &nN, &nL) == 2)
{
InitTrie(pRoot);
while (nN--)
{
scanf("%s", szPat);
Insert(pRoot, szPat);
}
BuildAC(pRoot, M);

Matrix tmp(1);
tmp.nD[0][0] = 26;
tmp = SumPowMatrix(tmp, nL);
INT nAns = tmp.nD[0][0];
Matrix msum = SumPowMatrix(M, nL);
for (int i = 0; i < msum.nSize; ++i)
{
nAns -= msum.nD[0][i];
}
printf("%I64u\n", nAns);
}

return 0;
}

题意很简单,假定文本集就是A,C,T,G,给定M个模式串,问你长度为N的文本不出现这些模式串的可能性到底有多少种。。。
确实非常不直观的样子。。。
解法是先学学AC自动机,建立起Trie图,根据trie图可以得到长度为1的路径矩阵,然后再快速冥得到长度为N的路径矩阵。
说起来都非常纠结,没学过AC自动机更加无法理解。学AC自动机之前据说得先学Trie树和KMP才好理解。学AC自动机搞Trie图就花费了近2天了,然后弄懂这个题又是一天,好在基本明白了。马上快比赛了,从长春换到金华也不知道是好是坏。。。还是弱菜啊。。。
贴下我的Trie图+快速冥(直接二分了,没有写成数论里面那种算法)…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <queue>
using namespace std;

typedef long long INT;
const int MOD = 100000;
const int MAX_P = 100;
const int MAX_D = 4;
int nIdx[256];
char szPat[MAX_P];
INT nMatrix[MAX_P][MAX_P];
INT B[MAX_P][MAX_P];
INT A[MAX_P][MAX_P];

void InitIdx()
{
nIdx['A'] = 0;
nIdx['C'] = 1;
nIdx['T'] = 2;
nIdx['G'] = 3;
}

struct Trie
{
Trie* fail;
Trie* next[MAX_D];
int no;
bool flag;
Trie()
{
fail = NULL;
memset(next, 0, sizeof(next));
no = 0;
flag = false;
}
};
Trie tries[MAX_D * MAX_P];
int nP;
Trie* pRoot;

Trie* NewNode()
{
memset(tries[nP], 0, sizeof(Trie));
tries[nP].no = nP;
return tries[nP++];
}

void InitTrie(Trie* pRoot)
{
nP = 0;
pRoot = NewNode();
}

void Insert(char* pszPat)
{
Trie* pNode = pRoot;

while (*pszPat)
{
if (pNode->next[nIdx[*pszPat]] == NULL)
{
pNode->next[nIdx[*pszPat]] = NewNode();
}
pNode = pNode->next[nIdx[*pszPat]];
++pszPat;
}
pNode->flag = true;
}

int BuildAC(Trie* pRoot)
{
memset(nMatrix, 0, sizeof(nMatrix));

pRoot->fail = NULL;
queue<Trie*> qt;
qt.push(pRoot);
while (!qt.empty())
{
Trie* front = qt.front();
qt.pop();

for (int i = 0; i < MAX_D; ++i)
{
if (front->next[i])
{
Trie* pNode = front->fail;
while (pNode && pNode->next[i] == NULL)
{
pNode = pNode->fail;
}
front->next[i]->fail = pNode? pNode->next[i] : pRoot;
if (front->next[i]->fail->flag == true)
{
front->next[i]->flag = true;
}

qt.push(front->next[i]);
}
else
{
front->next[i] = front == pRoot? pRoot : front->fail->next[i];
}

if (front->next[i]->flag == false)
{
nMatrix[front->no][front->next[i]->no]++;
}
}
}

return nP;//节点总个数
}

void MultyMatrix(INT A[][MAX_P], INT B[][MAX_P], INT C[][MAX_P], int nSize)
{
for (int i = 0; i < nSize; ++i)
{
for (int j = 0; j < nSize; ++j)
{
INT nSum = 0;
for (int k = 0; k < nSize; ++k)
{
nSum = (nSum + A[i][k] * B[k][j]) % MOD;
}
C[i][j] = nSum;
}
}
}

void CopyMatrix(INT A[][MAX_P], INT B[][MAX_P], int nSize)
{
for (int i = 0; i < nSize; ++i)
{
for (int j = 0; j < nSize; ++j)
{
A[i][j] = B[i][j];
}
}
}

void MatrixPower(INT M[][MAX_P], int nSize, INT nP)
{
if (nP == 1)
{
CopyMatrix(A, M, nSize);
return;
}

MatrixPower(M, nSize, nP / 2);
MultyMatrix(A, A, B, nSize);
if (nP % 2)
{
MultyMatrix(B, M, A, nSize);
}
else
{
CopyMatrix(A, B, nSize);
}
}

int main()
{
INT nM, nN;

InitIdx();
while (scanf("%I64d%I64d", &nM, &nN) == 2)
{
InitTrie(pRoot);
while (nM--)
{
scanf("%s", szPat);
Insert(szPat);
}
int nSize = BuildAC(pRoot);

MatrixPower(nMatrix, nSize, nN);
INT nAns = 0;
for (int i = 0; i < nSize; ++i)
{
nAns = (nAns + A[0][i]) % MOD;
}
printf("%I64d\n", nAns % MOD);
}

return 0;
}

句子的语法匹配。这个用DFA确实可以很方便做出来,用递归判断之类的应该也可以。感觉用dfa只需要保证状态转换图对了,基本上就不会出bug了,但是其它的方法去匹配这种类似正则表达式的字符串就容易出错多了。

百度百科的DFA定义如下:
英文全称:Deterministic Finite Automaton, 简写:DFA
DFA定义:一个确定的有穷自动机(DFA)M是一个五元组:M=(K,Σ,f,S,Z)其中
① K是一个有穷集,它的每个元素称为一个状态;
② Σ是一个有穷字母表,它的每个元素称为一个输入符号,所以也称Σ为输入符号字母表;
③ f是转换函数,是K×Σ→K上的映射,即,如 f(ki,a)=kj,(ki∈K,kj∈K)就意味着,
当前状态为ki,输入符为a时,将转换为下一个状态kj,我们把kj称作ki的一个后继状态;
④ S ∈ K是唯一的一个初态;
⑤ Z⊂K是一个终态集,终态也称可接受状态或结束状态。
该题的状态转换图:

现在再根据状态转换图,写一个模拟转换关系的匹配就非常方便了。。。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#include <string>
#include <vector>
#include <sstream>
#include <iostream>
#include <algorithm>
using namespace std;

string strNouns[8] =
{
"tom", "jerry", "goofy", "mickey",
"jimmy", "dog", "cat", "mouse"
};

bool IsNoun(string str)
{
for (int i = 0; i < 8; ++i)
{
if (str == strNouns[i])
{
return true;
}
}
return false;
}

bool IsVerb(string str)
{
return str == "hate" || str == "love"
|| str == "know" || str == "like"
|| str == "hates" || str == "loves"
|| str == "knows" || str == "likes";
}

bool IsArticle(string str)
{
return str == "a" || str == "the";
}

bool CheckState(vector<string> vs)
{
if (vs.empty()) return false;

int nState = 0;
for (int i = 0; i < vs.size(); ++i)
{
//printf("nState:%d, str:%s\n", nState, vs[i].c_str());
switch (nState)
{
case 0:
if (IsArticle(vs[i]))
{
nState = 1;
break;
}
else if (IsNoun(vs[i]))
{
nState = 2;
break;
}
else
{
return false;
}

case 1:
if (IsNoun(vs[i]))
{
nState = 2;
break;
}
else
{
return false;
}

case 2:
if (vs[i] == "and")
{
nState = 0;
break;
}
else if (IsVerb(vs[i]))
{
nState = 3;
break;
}
else
{
return false;
}

case 3:
if (IsArticle(vs[i]))
{
nState = 4;
break;
}
else if (IsNoun(vs[i]))
{
nState = 5;
break;
}
else
{
return false;
}

case 4:
if (IsNoun(vs[i]))
{
nState = 5;
break;
}
else
{
return false;
}

case 5:
if (vs[i] == "and")
{
nState = 3;
break;
}
else if (vs[i] == ",")
{
nState = 0;
break;
}
else
{
return false;
}
}
}

return nState == 5;
}

int main()
{
int nT;

scanf("%d%*c", &nT);
while (nT--)
{
vector<string> vs;
string line, str;

getline(cin, line);
stringstream ss(line);
while (ss >> str)
{
vs.push_back(str);
}
printf("%s\n", CheckState(vs) ? "YES I WILL" : "NO I WON'T");
}

return 0;
}

这题意思很简单。求一棵树里面的一条路径,使得其异或权(就是将路径里面所有边的权值异或起来)最大。
这个题有两步。第一步是假定根为节点0,求出根到其它节点的异或距离,保存在数组xor里面,这个dfs一下即可。然后,用xor[i]^xor[j]就能代表节点i到节点j的路径。这个结论可以这么看。如果,i和j之间的路径经过根节点,那么上面的结论肯定是正确的。如果,该路径不经过根,那么xor[i]和xor[j]必定保护从根到某个节点的相同的一段子路径,根据异或的性质,这段子路径会被消掉,所以这个结论也是这确的。。。
第二步就是枚举,xor[i]^xor[j]使得结果最大了。如果直接暴力,平方的算法肯定会超时的。
由于每个值可以表示成2进制,如果把其它xor值都保存在字典树里面,用当前的xor[i]去字典树
里面,一遍就可以找到异或最大值。
另外,由于树的点数太多,只能用邻接表,用vector模拟邻接表果断超时了。。。改成静态链表才过。。。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <vector>
using namespace std;

const int MAX = 100010;
int nXor[MAX];
bool bVis[MAX];
int nFirst[MAX];
struct Edge
{
int nE;
int nW;
int nNext;
};
Edge egs[MAX * 2];

struct Node
{
Node* pSons[2];
};
Node nodes[MAX * 32];
Node* pRoot = nodes[0];
int nNew;

void GetBCode(int nL, int* nBCode, int nLen)
{
nLen = 0;
while (nLen <= 30)
{
nBCode[nLen++] = nL % 2;
nL >>= 1;
}
reverse(nBCode, nBCode + nLen);
}

void Insert(int nL)
{
int nLen = 0;
int i = 0;
int nBCode[32];

GetBCode(nL, nBCode, nLen);
Node* pNode = pRoot;

while (i < nLen)
{
if (pNode->pSons[nBCode[i]])
{
pNode = pNode->pSons[nBCode[i]];
}
else
{
memset(nodes + nNew, 0, sizeof(nodes[nNew]));
pNode->pSons[nBCode[i]] = nodes + nNew;
pNode = nodes + nNew;
++nNew;
}
++i;
}
}

int FindMax(int nL)
{
int nLen = 0;
int nAns = 0;
int i = 0;
int nBCode[32];
Node* pNode = pRoot;

GetBCode(nL, nBCode, nLen);
while (i < nLen)
{
int nBest = (nBCode[i] == 0 ? 1 : 0);
int nBad = (nBCode[i] == 0 ? 0 : 1);
if (pNode->pSons[nBest])
{
nAns = 2 * nAns + nBest;
pNode = pNode->pSons[nBest];
}
else if (pNode->pSons[nBad])
{
nAns = 2 * nAns + nBad;
pNode = pNode->pSons[nBad];
}
else break;
++i;
}

return nAns ^ nL;
}

void Dfs(int nV, int nL)
{
nXor[nV] = nL;
bVis[nV] = true;
for (int e = nFirst[nV]; e != -1; e = egs[e].nNext)
{
if (!bVis[egs[e].nE])
{
Dfs(egs[e].nE, nL ^ egs[e].nW);
}
}
}

int main()
{
int nN;
int nU, nV, nW;

while (scanf("%d", &nN) == 1)
{
for (int i = 0; i < nN; ++i) nFirst[i] = -1;
for (int i = 1, j = 0; i < nN; ++i)
{
scanf("%d%d%d", &nU, &nV, &nW);
egs[j].nE = nV;
egs[j].nW = nW;
egs[j].nNext = nFirst[nU];
nFirst[nU] = j++;
egs[j].nE = nU;
egs[j].nW = nW;
egs[j].nNext = nFirst[nV];
nFirst[nV] = j++;
}

memset(bVis, false, sizeof(bool) * nN);
Dfs(0, 0);

memset(nodes[0], 0, sizeof(Node));
nNew = 1;
int nAns = 0;

for (int i = 0; i < nN; ++i)
{
nAns = max(nAns, FindMax(nXor[i]));
Insert(nXor[i]);
}
printf("%d\n", nAns);
}

return 0;
}

这是并查集最后一题,据说也是最经典的一题。经常前面几题的训练,这题的思路很快就能想出来了.只需要对每个节点附加一个信息表示离根节点的距离,并且距离是模3循环的。注意合并时候保持距离变化的正确性。而且合并有2种情况,距离相同合并和距离不同合并。分别对应于题目描述中的1和2操作。
关键还是FindSet里面对距离nDis数组里面的修改,前面一直写错这个,wa了好几次,还是看队友代码才一眼发现我又把这里写错了。。。当前距离的更新还是等于当前距离加上前一个节点的距离再模3,类似于前面几题的更新方法。
这种将有关系的节点放在一个并查集里面,再给每个节点附加其它信息描述其它关系的做法,确实比较有效。。。并查集是应用于不相交集合的数据结构,看来某个时候却有妙用啊。。。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;

const int MAX = 50010;
int nN, nK;
int nSets[MAX];
int nDis[MAX];

void MakeSets(int nN)
{
for (int i = 1; i <= nN; ++i)
{
nSets[i] = i;
nDis[i] = 0;
}
}

int FindSet(int nI)
{
if (nSets[nI] != nI)
{
int nPre = nSets[nI];
nSets[nI] = FindSet(nSets[nI]);
nDis[nI] = (nDis[nPre] + nDis[nI]) % 3;
}
return nSets[nI];
}

int main()
{
int nAns = 0;
int nOper, nX, nY;

scanf("%d%d", &nN, &nK);
MakeSets(nN);
while (nK--)
{
scanf("%d%d%d", &nOper, &nX, &nY);
if (nX > nN || nY > nN || nOper == 2 nX == nY)
{
++nAns;
}
else
{
if (nOper == 1)
{
int nA = FindSet(nX);
int nB = FindSet(nY);
if (nA == nB)
{
if (nDis[nX] != nDis[nY])
{
++nAns;
}
}
else
{
nSets[nB] = nA;
nDis[nB] = (nDis[nX] - nDis[nY] + 3) % 3;
}
}
else
{
int nA = FindSet(nX);
int nB = FindSet(nY);
if (nA == nB)
{
if ((nDis[nX] + 1) % 3 != nDis[nY])
{
++nAns;
}
}
else
{
nSets[nB] = nA;
nDis[nB] = (nDis[nX] + 1 - nDis[nY] + 3) % 3;
}
}
}
}
printf("%d\n", nAns);

return 0;
}

也是个题意比较奇葩的题目。有2个操作,1个是把一个元素所在的栈,放到另外1个元素所在的栈面。另外一个操作是统计某个元素下面有多少个元素(当然是在同一个栈中)。貌似,需要记录每个元素下面的元素是什了,既然要记录这个就不能用并查集的路径压缩了。不压缩路径的话,肯定会超时的。怎么办了。。。
其实,可以这么考虑,以每个栈的栈底元素作为并查集的代表元素。压缩路径后,每个元素或者是根元素或者其父亲元素就是根元素。所以,另外对每个节点附加个信息代表该节点的高度,栈底元素高度为0。再附加个信息代表每个并查集元素总数目,这样就可以在合并集合时候修改信息,并且压缩路径也能保证答案正确。。。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
using namespace std;

const int MAX = 30010;
int nSets[MAX];
int nNum[MAX];
int nRank[MAX];

void MakeSets(int nN)
{
for (int i = 0; i < nN; ++i)
{
nSets[i] = i;
nNum[i] = 1;
nRank[i] = 0;
}
}

int FindSet(int nI)
{
if (nSets[nI] != nI)
{
int nPre = nSets[nI];
nSets[nI] = FindSet(nSets[nI]);
nRank[nI] += nRank[nPre];
}

return nSets[nI];
}

void Move(int nX, int nY)
{
int nA = FindSet(nX);
int nB = FindSet(nY);
//printf("nA:%d,nB:%d\n", nA, nB);
if (nA != nB)
{
nSets[nA] = nB;
nRank[nA] += nNum[nB];
nNum[nB] += nNum[nA];
}
}

int main()
{
int nP;
char szOper[10];
int nX, nY;

scanf("%d", &nP);
MakeSets(MAX);
while (nP--)
{
scanf("%s", szOper);
if (szOper[0] == 'M')
{
scanf("%d%d", &nX, &nY);
Move(nX, nY);
}
else
{
scanf("%d", &nX);
FindSet(nX);
printf("%d\n", nRank[nX]);
}
}

return 0;
}

并查集应用的变形。题目意思是一个图中,只有上下左右四个方向的边。给出这样的一些边,求任意指定的2个节点之间的距离。
有可能当前给出的信息,没有涉及到要求的2个节点,或者只涉及到了1个节点,那么肯定无法确定它们的距离。或者根据已经给出的边只知道这2个节点在不同的联通分量里面,那么其距离也是无法确定的,根据题目要求,输出-1。
问题是如果能够确定它们在一个联通分量里面,如何确定它们的距离了。这个题的关键在于,只有上下左右四个方向的边,假设每个节点都有一个坐标的话,那么它们相对于代表该联通分量节点的坐标肯定是固定的,那么就不需要考虑图里面有环之类的情况了。这样就可以很方便的应用并查集来解了。
利用并查集,给每个节点附加其它信息,即相对于代表该并查集的节点的坐标(x,y)。在FindSet里面求出坐标,在UnionSet里面修改合并后新加入的另外一个集合的根节点的坐标即可。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;

const int MAX_N = 40010;
int nN, nM;
int nSets[MAX_N];
int nX[MAX_N];
int nY[MAX_N];
char szInput[MAX_N][100];

void MakeSets(int nNum)
{
for (int i = 0; i < nNum; ++i)
{
nSets[i] = i;
nX[i] = nY[i] = 0;
}
}

int FindSets(int nI)
{
if (nSets[nI] != nI)
{
int nPre = nSets[nI];
nSets[nI] = FindSets(nSets[nI]);
nX[nI] += nX[nPre];
nY[nI] += nY[nPre];
}
return nSets[nI];
}

void UnionSets(int nBeg, int nEnd, int dx, int dy)
{
int nA = FindSets(nBeg);
int nB = FindSets(nEnd);
if (nA != nB)
{
nSets[nB] = nA;//把集合B合并到集合A中
nX[nB] = nX[nBeg] + dx - nX[nEnd];//因为方向逆过来了,所以是减去
nY[nB] = nY[nBeg] + dy - nY[nEnd];
}
}

int main()
{
int nBeg, nEnd, nL;
char szDir[10];

while (scanf("%d%d%*c", &nN, &nM) == 2)
{
MakeSets(nN);
for (int i = 0; i < nM; ++i)
{
fgets(szInput[i], 100, stdin);
}
int nK;
int nF1, nF2, nI;
scanf("%d", nK);
int nCur = 0;
while (nK--)
{
scanf("%d%d%d", nF1, nF2, nI);
for (int i = nCur; i < nI; ++i)
{
sscanf(szInput[i], "%d%d%d%s", &nBeg,
&nEnd, &nL, szDir);
int dx = 0, dy = 0;
switch (szDir[0])
{
case 'N':
dy += nL;
break;
case 'S':
dy -= nL;
break;
case 'E':
dx += nL;
break;
case 'W':
dx -= nL;
break;
}
UnionSets(nBeg, nEnd, dx, dy);
}
nCur = nI;

if (FindSets(nF1) != FindSets(nF2))
{
printf("-1\n");
}
else
{
printf("%d\n", abs(nX[nF1] - nX[nF2])
+ abs(nY[nF1] - nY[nF2]));
}
}
}

return 0;
}

并查集应用的变形。
给出的是2个节点是敌对关系的信息,最后询问任意2个节点的关系。根据这些信息,
节点之间可能是敌对的,也可能不是的(因为敌人的敌人就是朋友),也可能给出的
信息根本描述不了它们的关系。
看起来跟原始的并查集应用差远了。。。
有个比较直接的做法,那么就是把不在一个集合的节点直接用并查集合并在一起。这样的话,
如果询问的2个节点在同一个并查集里面,那么它们之间的关系是确定的,否则无法确定它们的
关系。
现在还有一个问题是,在同一个并查集里面的2个节点是敌对关系还是朋友关系。。。
可以给每个节点另外附加个信息,记录其距离并查集根节点的距离。如果,询问的2个节点距离
其根节点的距离都是奇数或者都是偶数,那么这2个节点是朋友关系,否则是敌对关系。。。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;

const int MAX_N = 100010;
int nSets[MAX_N];
int nDis[MAX_N];

int nN, nM;

void MakeSets(int nNum)
{
for (int i = 0; i < nNum; ++i)
{
nSets[i] = i;
nDis[i] = 0;
}
}

int FindSet(int nI)
{
if (nSets[nI] != nI)
{
int nPre = nSets[nI];
nSets[nI] = FindSet(nSets[nI]);
nDis[nI] = (nDis[nI] + nDis[nPre]) % 2;
}
return nSets[nI];
}

void UnionSet(int nI, int nJ)
{
int nA = FindSet(nI);
int nB = FindSet(nJ);
if (nA != nB)
{
nSets[nA] = nB;
nDis[nA] = (nDis[nI] + nDis[nJ] + 1) % 2;
}
}

int main()
{
int nT;

scanf("%d", nT);
while (nT--)
{
scanf("%d%d", nN, nM);
MakeSets(nN);
char szOper[10];
int nA, nB;
while (nM--)
{
scanf("%s%d%d", szOper, &nA, &nB);
if (szOper[0] == 'D')
{
UnionSet(nA, nB);
}
else
{
int nX = FindSet(nA);
int nY = FindSet(nB);
if (nX == nY)
{
if (nDis[nA] == nDis[nB])
{
printf("In the same gang.\n");
}
else
{
printf("In different gangs.\n");
}
}
else
{
printf("Not sure yet.\n");
}
}
}
}

return 0;
}

此题本来模拟即可,但是注意有容易出错的地方。
这题主要是可以用中国剩余定理来做。
根据题意可以抽象出这样的模型。给出三个数A,B,C分别是模23,28,33后的余数,求最小的数字
使得其模23,28,33分别为A,B,C,并且要大于给定的数字D。
中国剩余定理很好的解决了这种余数问题。令模数为Ni,余数为Ai,设Mi = N1N2Ni-1Ni+1Nn,
那么答案一定满足形式ans = ΣAiMi(Mi对Ni的乘法逆) % N。(N为所有Ni的乘积)。
很明显,由于ans的第i项有Mi因子,所以模N1-Ni-1和Ni+1-Nn肯定是0,而AiMi(Mi对Ni的乘法逆) %Ni就是Ai。这样就满足了要求。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <vector>
using namespace std;

int Egcd(int nN, int nM, int nX, int nY)
{
if (nM == 0)
{
nX = 1, nY = 0;
return nN;
}
int nRet = Egcd(nM, nN % nM, nX, nY);
int nT = nX;
nX = nY;
nY = nT - (nN / nM) * nY;
return nRet;
}

int main()
{
int nA, nB, nC, nD;
int nDays = 21252;
int nCase = 1;

while (scanf("%d%d%d%d", &nA, &nB, &nC, &nD),
nA != -1 || nB != -1 || nC != -1 || nD != -1)
{
int nFirst = 0;
nA %= 23;
nB %= 28;
nC %= 33;
int nM1= 28 * 33, nM2 = 23 * 33, nM3 = 23 * 28;
int nN1, nN2, nN3, nTemp;
Egcd(23, nM1, nTemp, nN1);
Egcd(28, nM2, nTemp, nN2);
Egcd(33, nM3, nTemp, nN3);
nFirst = (nA * nM1 * nN1 + nB * nM2 * nN2 + nC * nM3 * nN3) % nDays;
while (nFirst <= nD)nFirst += nDays;
printf("Case %d: the next triple peak occurs in %d days.\n",
nCase++, nFirst - nD);
}

return 0;
}

这个题是求一个字符串的最小重复单元的重复次数,那么求出最小重复单元的长度即可。这个题有3种方法,方法一:直接枚举长度为[1,len/2]内的子串,暴力就过了。方法二:将原串重复一次形成一个新串,用原串去匹配新串,但是得从第二个位置开始匹配,第一次成功匹配的位置减一就代表最小重复单元的长度。方法三:利用kmp的next函数,如果len能够整除len-next[len],那么len-next[len]就代表最小重复单元的长度。
方法一明显是对的,数据不强的情况下就能水过了。方法二也不是那么容易想到的,不过将原串扩展为2倍的做法也不是太奇葩,比如判断2个循环串是否相等就可以用这个办法做。
方法三就比较难理解了。
方法三的理解:
next[len]代表的是str的最长前缀(使得这个前缀与同样长度的后缀相等)的长度。所谓的next
数组就是长度为1-len的str的满足上述描述的最长前缀的长度。如果len是len-next[len]的倍数,假设m= len-next[len] ,那么str[1-m] = str[m-2 m],以此类推下去,m肯定是str的最小重复单元的长度。假如len不是len-next[len]的倍数, 如果前缀和后缀重叠,那么最小重复单元肯定str本身了,如果前缀和后缀不重叠,那么str[m - 2 m] != str[len-m,len],所以str[1-m]!= str[m - 2 * m] ,最终肯定可以推理出最小重复单元是str本身,因为只要不断递增m证明即可。

方法三的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;

char szStr[1000010];
int nNext[1000010];

void GetNext(char* szStr, int nLen, int* nNext)
{
nNext[0] = -1;
for (int i = 1, j = -1; i < nLen; ++i)
{
while (j > -1 szStr[i] != szStr[j + 1])
{
j = nNext[j];
}
if (szStr[i] == szStr[j + 1])
{
++j;
}
nNext[i] = j;
}
}

int main()
{
while (scanf("%s", szStr), strcmp(szStr, "."))
{
int nLen = strlen(szStr);

GetNext(szStr, nLen, nNext);
if (nLen % (nLen - nNext[nLen - 1] - 1))
{
printf("1\n");
}
else
{
printf("%d\n", nLen / (nLen - nNext[nLen - 1] - 1));
}
}

return 0;
}