AC自动机可以看作是在Trie树上建立了fail指针,在这里可以看作fail链。如果u的fail链指向v,那么v的对应串一定是u对应串在所给定字符串集合的后缀集合中的最长的后缀。

我们考虑一下如何实现这个东西。

以上数组实现的过程中我们让0号结点充当了根,这样会省去很多边界的处理。

我们考虑如何用指针去实现AC自动机,这看起来要更加自然。在指针实现的过程中,为了严谨起见,我们定义:节点的ch指针在默认情况下为NULL,fail指针在默认情况下为root(根节点自身除外)

这样的话就更加严格,各种边界情况都非常清晰。

#10057. 「一本通 2.4 例 1」Keywords Search

#include <bits/stdc++.h>
using namespace std; namespace ACA {
struct Node {
Node *ch[], *fail;
int val;
// Default value: fail=root ch=NULL
Node* clear(Node* defFail) {
fail = defFail;
for (int i = ; i < ; i++) ch[i] = ;
val = ;
return this;
}
};
Node *root = NULL, pool[];
int index;
Node* newnode() { return pool[index++].clear(root); }
void insert(string str) {
Node* p = root;
for (int i = ; i < str.length(); i++) {
int v = str[i] - 'a';
if (!p->ch[v])
p->ch[v] = newnode();
p = p->ch[v];
}
p->val++;
}
void build() {
queue<Node*> q;
for (int i = ; i < ; i++)
if (root->ch[i])
q.push(root->ch[i]);
while (!q.empty()) {
Node* p = q.front();
q.pop(); for (int i = ; i < ; i++) {
if (p->ch[i] != NULL)
p->ch[i]->fail = (p->fail->ch[i]) ? ((p->fail->ch[i])) : root, q.push(p->ch[i]);
else
p->ch[i] = p->fail->ch[i];
}
}
}
int query(string str) {
Node* p = root;
int ans = ;
for (int i = ; i < str.length(); i++) {
p = p->ch[str[i] - 'a'];
if (p == NULL)
p = root; // Attention!
for (Node* t = p; t != root && ~(t->val); t = t->fail) ans += t->val, t->val = -;
}
return ans;
}
void solve() {
int n;
cin >> n;
index = ;
root = newnode();
for (int i = ; i <= n; i++) {
string tmp;
cin >> tmp;
insert(tmp);
}
build();
string art;
cin >> art;
cout << query(art) << endl;
}
} // namespace ACA int main() {
int T;
// ios::sync_with_stdio(false);
cin >> T;
while (T--) ACA::solve();
}

#10058. 「一本通 2.4 练习 1」玄武密码

我们考虑query()中的那个while循环,它沿着当前节点到根的fail链跳,跳到不能再跳或者遇到已经处理过的节点为止。所谓处理节点,相当于标记这个节点对应的串已经被匹配。根据这个特性很容易构造出算法。

#include <bits/stdc++.h>
using namespace std; namespace ACA {
struct Node {
Node *ch[], *fail;
int val;
// Default value: fail=root ch=NULL
Node* clear(Node* defFail) {
fail = defFail;
for (int i = ; i < ; i++) ch[i] = ;
val = ;
return this;
}
};
Node *root = NULL, pool[];
int index;
Node* newnode() { return pool[index++].clear(root); }
void insert(string str) {
Node* p = root;
for (int i = ; i < str.length(); i++) {
int v = str[i];
if (!p->ch[v])
p->ch[v] = newnode();
p = p->ch[v];
p->val = max(p->val, i);
}
}
void build() {
queue<Node*> q;
for (int i = ; i < ; i++)
if (root->ch[i])
q.push(root->ch[i]);
while (!q.empty()) {
Node* p = q.front();
q.pop();
for (int i = ; i < ; i++) {
if (p->ch[i] != NULL)
p->ch[i]->fail = (p->fail->ch[i]) ? ((p->fail->ch[i])) : root, q.push(p->ch[i]);
else
p->ch[i] = p->fail->ch[i];
}
}
}
void query(string str) {
Node* p = root;
for (int i = ; i < str.length(); i++) {
p = p->ch[str[i]];
if (p == NULL)
p = root; // Attention!
for (Node* t = p; t != root && ~(t->val); t = t->fail) t->val = -;
}
}
int getans(string str) {
Node* p = root;
for (int i = ; i < str.length(); i++) {
p = p->ch[str[i]];
if (p->val >= )
return i;
}
return str.length();
}
string pat[];
void solve() {
int n;
cin >> n;
cin >> n;
index = ;
root = newnode();
string art;
cin >> art;
for (int j = ; j < art.length(); j++) {
if (art[j] == 'S')
art[j] = ;
if (art[j] == 'N')
art[j] = ;
if (art[j] == 'W')
art[j] = ;
if (art[j] == 'E')
art[j] = ;
}
for (int i = ; i <= n; i++) {
string tmp;
cin >> tmp;
for (int j = ; j < tmp.length(); j++) {
if (tmp[j] == 'S')
tmp[j] = ;
if (tmp[j] == 'N')
tmp[j] = ;
if (tmp[j] == 'W')
tmp[j] = ;
if (tmp[j] == 'E')
tmp[j] = ;
}
insert(tmp);
pat[i] = tmp;
}
build();
query(art);
for (int i = ; i <= n; i++) cout << getans(pat[i]) << endl;
}
} // namespace ACA int main() {
ios::sync_with_stdio(false);
int T;
ACA::solve();
}

#10059. 「一本通 2.4 练习 2」Censoring

之前KMP专题里面出现过一个censoring的题目,只不过在这里我们把单串换成了多串,但思想是一致的。

搞一个输出栈,每次删除时候弹栈,栈中同时记录一下匹配到的节点即可。

注意到这里由于子串不会互相包含,所以query中的while就可以去掉了。

写的时候忘记给staj[0]赋值T掉了,要注意边界

#include <bits/stdc++.h>
using namespace std; namespace ACA {
struct Node {
Node *ch[], *fail;
int val;
// Default value: fail=root ch=NULL
Node* clear(Node* defFail) {
fail = defFail;
for (int i = ; i < ; i++) ch[i] = ;
val = ;
return this;
}
};
Node *root = NULL, pool[];
int index;
int sta[], top = ;
Node* stap[];
Node* newnode() { return pool[index++].clear(root); }
void insert(string& str) {
Node* p = root;
for (int i = ; i < str.length(); i++) {
int v = str[i] - 'a';
if (!p->ch[v])
p->ch[v] = newnode();
p = p->ch[v];
}
p->val = str.length();
}
void build() {
queue<Node*> q;
for (int i = ; i < ; i++)
if (root->ch[i])
q.push(root->ch[i]);
while (!q.empty()) {
Node* p = q.front();
q.pop();
for (int i = ; i < ; i++) {
if (p->ch[i] != NULL)
p->ch[i]->fail = (p->fail->ch[i]) ? ((p->fail->ch[i])) : root, q.push(p->ch[i]);
else
p->ch[i] = p->fail->ch[i];
}
}
}
int query(string& str) {
Node* p = root;
int ans = ;
stap[] = root; // Warning!!!
for (int i = ; i < str.length(); i++) {
p = p->ch[str[i] - 'a'];
if (p == NULL)
p = root; // Attention!
++top;
sta[top] = i;
stap[top] = p;
if (p->val > ) {
top -= p->val;
p = stap[top];
}
}
for (int i = ; i <= top; i++) cout << str[sta[i]];
return ans;
}
void solve() {
string art;
cin >> art;
int n;
cin >> n;
index = ;
root = newnode();
for (int i = ; i <= n; i++) {
string tmp;
cin >> tmp;
insert(tmp);
}
build();
query(art);
}
} // namespace ACA int main() {
ios::sync_with_stdio(false);
ACA::solve();
}

#10060. 「一本通 2.4 练习 3」单词

某人读论文,一篇论文是由许多单词组成。但他发现一个单词会在论文中出现很多次,现在想知道每个单词分别在论文中出现多少次。

正常插入所有单词串,不过对每个位置都要打标记,这样相当于插入了所有的前缀。构建fail树完成后,对fail树求树上前缀和(自底向上),此后每个节点的值就是这个节点代表的串在整个字典中的出现次数。

#include <bits/stdc++.h>
using namespace std; namespace ACA {
struct Node {
Node *ch[], *fail;
int val, cnt;
vector<Node*> cf;
// Default value: fail=root ch=NULL
Node* clear(Node* defFail) {
fail = defFail;
for (int i = ; i < ; i++) ch[i] = ;
val = ;
cnt = ;
return this;
}
};
Node *root = NULL, pool[];
Node* epos[];
int index;
Node* newnode() { return pool[index++].clear(root); }
void insert(int id, string str) {
Node* p = root;
for (int i = ; i < str.length(); i++) {
int v = str[i] - 'a';
if (!p->ch[v])
p->ch[v] = newnode();
p = p->ch[v];
p->val++;
} epos[id] = p;
}
void build() {
queue<Node*> q;
vector<Node*> tq;
for (int i = ; i < ; i++)
if (root->ch[i])
q.push(root->ch[i]), tq.push_back(root->ch[i]);
while (!q.empty()) {
Node* p = q.front();
q.pop();
for (int i = ; i < ; i++) {
if (p->ch[i] != NULL)
p->ch[i]->fail = (p->fail->ch[i]) ? ((p->fail->ch[i])) : root, q.push(p->ch[i]),
tq.push_back(p->ch[i]);
else
p->ch[i] = p->fail->ch[i];
}
}
for (int i = tq.size() - ; i >= ; --i)
if (tq[i]->fail)
tq[i]->fail->val += tq[i]->val;
}
string s[];
void solve() {
int n;
cin >> n;
index = ;
root = newnode();
for (int i = ; i <= n; i++) {
string tmp;
cin >> tmp;
insert(i, tmp);
s[i] = tmp;
}
build();
for (int i = ; i <= n; i++) cout << epos[i]->val << endl;
}
} // namespace ACA int main() {
ios::sync_with_stdio(false);
ACA::solve();
}

#10061. 「一本通 2.4 练习 4」最短母串

输入N个串,找到它们的最短母串,输出可行解中字典序最小的。

第一次写这种题真的卡了很久。

我们最终应当是在AC自动机上跑记忆化BFS,状态的表示上需要借助状态压缩。

建树的时候,对于每一条fail边,假设从u指向v,即v是u的后缀,那么如果v是一个单词的结尾,那么u也应当是。这样看来我们需要对节点自身的状态做一下fail树上自顶向下的前缀和。

搜索的时候,维护BFS队列,顺便记录每个状态的来源信息方便输出。

#include <bits/stdc++.h>
using namespace std; const int N = ; int ch[N][], fi[N], val[N], n, m, t1, t2, t3, t4, ind; void ins(char *s, int id) {
int len = strlen(s), p = ;
for (int i = ; i < len; i++) {
if (ch[p][s[i] - 'A'] == )
ch[p][s[i] - 'A'] = ++ind;
p = ch[p][s[i] - 'A'];
}
val[p] |= << (id - );
} void build() {
queue<int> q;
for (int i = ; i < ; i++)
if (ch[][i])
q.push(ch[][i]);
while (!q.empty()) {
int p = q.front();
q.pop();
for (int i = ; i < ; i++)
if (ch[p][i])
fi[ch[p][i]] = ch[fi[p]][i], q.push(ch[p][i]);
else
ch[p][i] = ch[fi[p]][i];
int t = fi[p];
while (t && !val[t]) t = fi[t];
val[p] |= val[t];
}
} char str[N];
struct Status {
int pos = , state = , cnt = ;
Status(int a, int b, int c) : pos(a), state(b), cnt(c){};
};
int u[][ << ];
pair<int, int> fa[][ << ];
char o[][ << ];
string out;
int main() {
cin >> n;
for (int i = ; i <= n; i++) cin >> str, ins(str, i);
build();
queue<Status> q;
q.push((Status){ , val[], });
while (!q.empty()) {
Status p = q.front();
q.pop();
int node = p.pos, cnt = p.cnt, state = p.state;
if (state == ( << n) - ) {
while (node) {
out += o[node][state];
pair<int, int> tmp = fa[node][state];
node = tmp.first;
state = tmp.second;
}
reverse(out.begin(), out.end());
cout << out << endl;
return ;
}
for (int i = ; i < ; i++) {
int newstate = state | val[ch[node][i]];
int newnode = ch[node][i];
if (u[newnode][newstate])
continue;
u[newnode][newstate] = ;
o[newnode][newstate] = i + 'A';
fa[newnode][newstate] = make_pair(node, state);
q.push((Status){ newnode, newstate, });
}
}
}

#10062. 「一本通 2.4 练习 5」病毒

找到一个最小的不包含任何给定串的无限长串。

很显然这个串一定从某个位置开始循环。我们先对所有的终止节点打一个禁止标记,然后再fail树上跑自顶向下前缀和(因为终止节点再fail树上的所有孩子都应该被禁止),然后在所有没有被静止的节点间DFS,直到找到一条后向边。

#include <bits/stdc++.h>
using namespace std; const int N = ; int ch[N][], fail[N], val[N], u[N], n, m, t1, t2, t3, t4, ind;
vector<int> g[N]; void insert(char *s) {
int len = strlen(s), p = ;
for (int i = ; i < len; i++) {
if (ch[p][s[i] - ''] == )
ch[p][s[i] - ''] = ++ind;
p = ch[p][s[i] - ''];
}
val[p]++;
}
void build() {
queue<int> q;
for (int i = ; i < ; i++)
if (ch[][i])
q.push(ch[][i]);
while (!q.empty()) {
int p = q.front();
q.pop();
for (int i = ; i < ; i++) {
if (ch[p][i])
fail[ch[p][i]] = ch[fail[p]][i], q.push(ch[p][i]);
else
ch[p][i] = ch[fail[p]][i];
}
val[p] |= val[fail[p]];
}
for (int i = ; i <= ind; i++) {
// if(fail[i]) g[i].push_back(fail[i]);
g[i].push_back(ch[i][]);
g[i].push_back(ch[i][]);
}
}
bool dfs(int p) {
if (val[p])
return false;
// cout<<"dfs in "<<p<<endl;
u[p] = ;
for (int i = ; i < g[p].size(); i++) {
if (u[g[p][i]] == )
continue;
if (u[g[p][i]] == )
return true;
if (dfs(g[p][i]))
return true;
}
u[p] = ; // cout<<"dfs out "<<p<<endl;
return false;
}
char str[N];
int main() {
ios::sync_with_stdio(false);
cin >> n;
for (int i = ; i <= n; i++) cin >> str, insert(str);
build();
cout << (dfs() ? "TAK" : "NIE") << endl;
}

#10063. 「一本通 2.4 练习 6」文本生成器

老套路打禁止标记后做出自顶向下前缀和,然后暴力递推,设f[i][j]表示i步走到第j个节点,每次利用f[i][j]去刷所有f[i][j]的儿子,遇到禁止节点就将它的f[i][j]强行弄成0.最后求和即可。

#include <bits/stdc++.h>
using namespace std; const int N = ; int ch[N][], fail[N], val[N], u[N], n, m, t1, t2, t3, t4, ind;
vector<int> g[N];
int f[N][];
void insert(char *s) {
int len = strlen(s), p = ;
for (int i = ; i < len; i++) {
if (ch[p][s[i] - 'A'] == )
ch[p][s[i] - 'A'] = ++ind;
p = ch[p][s[i] - 'A'];
}
val[p]++;
}
void build() {
queue<int> q;
for (int i = ; i < ; i++)
if (ch[][i])
q.push(ch[][i]);
while (!q.empty()) {
int p = q.front();
q.pop();
for (int i = ; i < ; i++) {
if (ch[p][i])
fail[ch[p][i]] = ch[fail[p]][i], q.push(ch[p][i]);
else
ch[p][i] = ch[fail[p]][i];
}
val[p] |= val[fail[p]];
}
for (int i = ; i <= ind; i++) {
for (int j = ; j < ; j++) g[i].push_back(ch[i][j]);
}
} char str[N];
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = ; i <= n; i++) cin >> str, insert(str);
build();
f[][] = ;
for (int j = ; j <= m; j++) {
for (int i = ; i <= ind; i++) {
if (val[i]) {
f[i][j] = ;
continue;
}
for (int k = ; k < g[i].size(); k++) {
(f[g[i][k]][j + ] += f[i][j]) %= ;
}
}
}
int ans = ;
for (int i = ; i <= ind; i++) (ans += f[i][m]) %= ;
int tot = ;
for (int i = ; i <= m; i++) (tot *= ) %= ;
cout << ((tot - ans) % + ) % << endl;
}

总结

AC自动机的这个专题是到目前为止做的最艰难的一个专题。原因在于一些基本的性质理解得不透彻,以及在指针实现的几道题目中因为对边界考虑得不够清楚导致各种诡异情况。

从实际角度来看,数组版本其实比较适合绝大多数题目。

应用方面,我们用AC自动机来解决的基本问题其实就是多串匹配,即给定一个单词本,看文章中出现了单词本中的多少个单词。这是很容易的。

如果我们要分别统计每个单词出现了多少次,就需要用到fail树上前缀和的处理技巧。我们知道,从某个节点沿着fail树走到根,经过的节点都是这个节点的后缀(这里我们已经用节点来直接指代节点对应的串),那么如果某个节点出现,它到根的fail链上所有的节点都应当已经出现,如果这些节点是某个单词的结尾,那么这个单词也应该被统计。因此,我们每次修改实际上修改的是fail树上某个点到根的一条链,我们很容易想到把这转化为在该点上打一个标记然后自底向上前缀和。

这个思路非常重要,练习3中我们要统计每个单词在单词本里以子串形式出现的次数时,我们用的就是这种思路。如果p点出现,那么显然fail[p],fail[fail[p]]都也出现。所以去做自底向上前缀和。

对于Censoring这样的问题,我们需要把输出构建成一个栈,并且维护栈中每个位置对应的当时所在自动机上节点的编号,这样如果弹出我们就可以很快的转移到弹出后的状态。

AC自动机上进行搜索和dp的难度就会稍高一些。总得来说,这类问题需要去记录状态,这个状态通常包含对节点的描述和对某些其它信息的描述。通常还需要结合树上前缀和。

#include <bits/stdc++.h>
using namespace std; const int N = 1000005; int ch[N][26], fi[N], val[N], n, m, t1, t2, t3, t4, ind; void ins(char *s, int id) {
int len = strlen(s), p = 0;
for (int i = 0; i < len; i++) {
if (ch[p][s[i] - 'A'] == 0)
ch[p][s[i] - 'A'] = ++ind;
p = ch[p][s[i] - 'A'];
}
val[p] |= 1 << (id - 1);
} void build() {
queue<int> q;
for (int i = 0; i < 26; i++)
if (ch[0][i])
q.push(ch[0][i]);
while (!q.empty()) {
int p = q.front();
q.pop();
for (int i = 0; i < 26; i++)
if (ch[p][i])
fi[ch[p][i]] = ch[fi[p]][i], q.push(ch[p][i]);
else
ch[p][i] = ch[fi[p]][i];
int t = fi[p];
while (t && !val[t]) t = fi[t];
val[p] |= val[t];
}
} char str[N];
struct Status {
int pos = 0, state = 0, cnt = 0;
Status(int a, int b, int c) : pos(a), state(b), cnt(c){};
};
int u[1005][1 << 12];
pair<int, int> fa[1005][1 << 12];
char o[1005][1 << 12];
string out;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> str, ins(str, i);
build();
queue<Status> q;
q.push((Status){ 0, val[0], 0 });
while (!q.empty()) {
Status p = q.front();
q.pop();
int node = p.pos, cnt = p.cnt, state = p.state;
if (state == (1 << n) - 1) {
while (node) {
out += o[node][state];
pair<int, int> tmp = fa[node][state];
node = tmp.first;
state = tmp.second;
}
reverse(out.begin(), out.end());
cout << out << endl;
return 0;
}
for (int i = 0; i < 26; i++) {
int newstate = state | val[ch[node][i]];
int newnode = ch[node][i];
if (u[newnode][newstate])
continue;
u[newnode][newstate] = 1;
o[newnode][newstate] = i + 'A';
fa[newnode][newstate] = make_pair(node, state);
q.push((Status){ newnode, newstate, 0 });
}
}
}

[一本通学习笔记] AC自动机的更多相关文章

  1. 学习笔记::AC自动机

    最先开始以为和自动刷题机是一个东西... 其实就是kmp的一个拓展.学完kmp再学这个就会发现其实不难 1.kmp是一个串匹配一个串,但是当我们想用多个串匹配一个文本的时候,kmp就不行了,因此我们有 ...

  2. SAM学习笔记&AC自动机复习

    形势所迫,一个对字符串深恶痛绝的鸽子又来更新了. SAM 后缀自动机就是一个对于字符串所有后缀所建立起的自动机.一些优良的性质可以使其完成很多字符串的问题. 其核心主要在于每个节点的状态和$endpo ...

  3. [学习笔记]后缀自动机SAM

    好抽象啊,早上看了两个多小时才看懂,\(\%\%\%Fading\) 早就懂了 讲解就算了吧--可以去看看其他人的博客 1.[模板]后缀自动机 \(siz\) 为该串出现的次数,\(l\) 为子串长度 ...

  4. [一本通学习笔记] 最近公共祖先LCA

    本节内容过于暴力没什么好说的.借着这个专题改掉写倍增的陋习,虽然写链剖代码长了点不过常数小还是很香. 10130. 「一本通 4.4 例 1」点的距离 #include <bits/stdc++ ...

  5. [一本通学习笔记] RMQ专题

    傻傻地敲了好多遍ST表. 10119. 「一本通 4.2 例 1」数列区间最大值 #include <bits/stdc++.h> using namespace std; const i ...

  6. [一本通学习笔记] 字典树与 0-1 Trie

    字典树中根到每个结点对应原串集合的一个前缀,这个前缀由路径上所有转移边对应的字母构成.我们可以对每个结点维护一些需要的信息,这样即可以去做很多事情. #10049. 「一本通 2.3 例 1」Phon ...

  7. [一本通学习笔记] KMP算法

    KMP算法 对于串s[1..n],我们定义fail[i]表示以串s[1..i]的最长公共真前后缀. 我们首先考虑对于模式串p,如何计算出它的fail数组.定义fail[0]=-1. 根据“真前后缀”的 ...

  8. AC自动机--summer-work之我连模板题都做不出

    这章对现在的我来说有点难,要是不写点东西,三天后怕是就一无所有了. 但写这个没有营养的blog的目的真的不是做题或提升,只是学习学习代码和理解一些概念. 现在对AC自动机的理解还十分浅薄,这里先贴上目 ...

  9. Keywords Search - HDU 2222(AC自动机模板)

    题目大意:输入几个子串,然后输入一个母串,问在母串里面包含几个子串.   分析:刚学习的AC自动机,据说这是个最基础的模板题,所以也是用了最基本的写法来完成的,当然也借鉴了别人的代码思想,确实是个很神 ...

随机推荐

  1. JAVA JDK keytool 生成数字证书

    简介: 数字证书作为网络安全数据传输的凭证,web在传输时客户端(浏览器)和 服务端(服务器)先进行会话握手,在握手过程中服务端会验证客户端的是否已经在服务端做了认证,这是单向认证.如果是双向认证的话 ...

  2. 首次使用Lambda表达式-sunziren

    需要将List<Apple> list = new ArrayList<Apple>(); 按照Apple对象中的price属性从大到小排序. 第一个念头闪过的是冒泡排序,转念 ...

  3. 腾讯qlv视频转为MP4格式工具

    本文解决上一篇<优酷爱奇艺视频转换为MP4格式工具>留下的腾讯视频qlv转MP4格式问题,教程都是一步步亲手操作的,每一步都有配图.希望各位老板多转发分享,谢谢! 解压软件.(建议关闭所有 ...

  4. 自定义配置JNOJ

    OJ 全名 online judge 在线判题系统,对于从事编程竞赛的人来说一点都不陌生,今天我们讨论的是怎么样自定义搭建 推荐的开源的OJ有hustOJ,JNOJ 因为hustOJ 是一键安装脚本, ...

  5. 剑指offer-面试题39-数组中出现次数超过一半的数字-抵消法

    /* 题目: 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字. 例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}.由于数字2在数组中出现了5次,超过数组长度的一半,因此输 ...

  6. 如何在Mac电脑上隐藏视频文件?

    我们都有一些秘密视频,我们只想保留在Mac,iPhone或iPad上.为了完全安全地在Mac上隐藏视频文件,我们提供了两种种最简单的方法.下面就来看一下,如何在Mac上隐藏私密视频文件? 在iTune ...

  7. bootstrap 兼容 IE8

    在 html 中引用 <!-- bootstrap 兼容 IE8 --> <script src="../../jsapi/js/html5shiv.min.js" ...

  8. Pikachu-目录遍历

    目录遍历漏洞概述 在web功能设计中,很多时候我们会要将需要访问的文件定义成变量,从而让前端的功能便的更加灵活. 当用户发起一个前端的请求时,便会将请求的这个文件的值(比如文件名称)传递到后台,后台再 ...

  9. JavaSE学习笔记(4)---抽象类和接口

    JavaSE学习笔记(4)---抽象类和接口 抽象方法和抽象类 ·抽象方法 使用abstract修饰的方法,没有方法体,只有声明.定义的是一种"规范",就是告诉子类必须要给抽象方法 ...

  10. hextorgb

    function hexToRgb(hex) { // By Tim Down - http://stackoverflow.com/a/5624139/3493650 // Expand short ...