期末考试加军训,大概一个月没碰了
现在寒假开启了,开始训练
集训前有三个星期,就刷cf题吧
目标不高,25道比自己水平高一些的题目
考前两三天可以练一练模板
搞起。
1.30(Tire树)
做了D. Vasiliy's Multiset
独立思考了挺久没做出来
看题解发现要用字典树
那就复习一波字典树
就是这样,通过题目来巩固相关知识点
Tire树模板
理解为字符前缀树更好一些
可以快速处理字符的前缀问题
注意根节点是0
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
int tire[MAXN][30], n, cnt;
bool End[MAXN];
void add(char *str)
{
int len = strlen(str), p = 0;
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!tire[p][id]) tire[p][id] = ++cnt;
p = tire[p][id];
}
End[p] = true;
}
bool search(char *str)
{
int len = strlen(str), p = 0;
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!tire[p][id]) return false;
p = tire[p][id];
}
return End[p];
}
int main()
{
char str[100];
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%s", str);
add(str);
}
while(~scanf("%s", str))
{
if(search(str)) puts("Yes");
else puts("No");
}
return 0;
}
hdu 1251
这题的输入方式比较奇葩
gets遇到回车就停,所以要判断一下空串
用建立tire树后树形dp一次就ok了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int tire[MAXN][30], End[MAXN], ans[MAXN], cnt;
int dfs(int x)
{
int res = End[x];
_for(i, 0, 25)
if(tire[x][i])
res += dfs(tire[x][i]);
return ans[x] = res;
}
void add(char *str)
{
int len = strlen(str), p = 0;
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!tire[p][id]) tire[p][id] = ++cnt;
p = tire[p][id];
}
End[p]++;
}
int search(char *str)
{
int len = strlen(str), p = 0;
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!tire[p][id]) return 0;
p = tire[p][id];
}
return ans[p];
}
int main()
{
char str[100];
while(gets(str) && *str) add(str);
dfs(0);
while(~scanf("%s", str))
printf("%d\n", search(str));
return 0;
}
poj 3630
比较裸的一道题
用字典树处理字符集前缀问题
注意开数组的时候是单词数乘上每个单词的长度
不要只开单词数
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 + 10;
int tire[MAXN][15], n, cnt, ans;
bool End[MAXN];
void add(char *str)
{
int len = strlen(str), p = 0, flag = 1, flag2 = 1;
REP(i, 0, len)
{
int id = str[i] - '0';
if(!tire[p][id]) tire[p][id] = ++cnt, flag = 0;
p = tire[p][id];
if(End[p]) flag2 = 0;
}
if(flag || !flag2) ans = false;
End[p] = true;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
cnt = 0; ans = true;
memset(tire, 0, sizeof(tire));
memset(End, false, sizeof(End));
scanf("%d", &n);
_for(i, 1, n)
{
char str[15];
scanf("%s", str);
add(str);
}
if(ans) puts("YES");
else puts("NO");
}
return 0;
}
「一本通 2.3 例 2」The XOR Largest Pair
字典树还可以用来求异或最大值
用到了一些位运算
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 * 33;
int tire[MAXN][2], p, cnt, n;
void add(int x)
{
int p = 0;
for(int i = 30; i >= 0; i--)
{
int id = (x >> i) & 1;
if(!tire[p][id]) tire[p][id] = ++cnt;
p = tire[p][id];
}
}
int work(int x)
{
int p = 0, res = 0;
for(int i = 30; i >= 0; i--)
{
int id = (x >> i) & 1;
if(tire[p][id ^ 1])
{
res |= (1 << i);
p = tire[p][id ^ 1];
}
else p = tire[p][id];
}
return res;
}
int main()
{
int ans = 0;
scanf("%d", &n);
_for(i, 1, n)
{
int x; scanf("%d", &x);
add(x);
ans = max(ans, work(x));
}
printf("%d\n", ans);
return 0;
}
CF706D Vasiliy's Multiset
现在回到原来的题
其实学完前面的,这题不难的
其实就是上一题多了一个删除操作而已
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 2e5 * 33;
int tire[MAXN][2], vis[MAXN], cnt, q;
void add(int x)
{
int p = 0;
for(int i = 30; i >= 0; i--)
{
int id = (x >> i) & 1;
if(!tire[p][id]) tire[p][id] = ++cnt;
p = tire[p][id];
vis[p]++;
}
}
void erase(int x)
{
int p = 0;
for(int i = 30; i >= 0; i--)
{
int id = (x >> i) & 1;
p = tire[p][id];
vis[p]--;
}
}
int work(int x)
{
int p = 0, res = 0;
for(int i = 30; i >= 0; i--)
{
int id = (x >> i) & 1;
if(tire[p][id ^ 1] && vis[tire[p][id ^ 1]])
{
res |= (1 << i);
p = tire[p][id ^ 1];
}
else p = tire[p][id];
}
return max(x, res);
}
int main()
{
scanf("%d", &q);
while(q--)
{
char op[5]; int x;
scanf("%s%d", op, &x);
if(op[0] == '+') add(x);
if(op[0] == '-') erase(x);
if(op[0] == '?') printf("%d\n", work(x));
}
return 0;
}
CF706C Hard problem
中途去做了一道有点水的dp题
我用的字符数组做的,一些操作挺麻烦的
用C++的string做很多操作会好写很多
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
char now[MAXN], pre1[MAXN], pre2[MAXN], t1[MAXN], t2[MAXN];
ll dp[MAXN][2];
int c[MAXN], n;
void copy(char a[], char b[])
{
memset(a, 0, sizeof(a));
strcpy(a, b);
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &c[i]);
_for(i, 1, n)
{
scanf("%s", now);
copy(t1, now);
reverse(now, now + strlen(now));
copy(t2, now);
dp[i][0] = dp[i][1] = 1e18;
if(strcmp(pre1, t1) <= 0) dp[i][0] = min(dp[i][0], dp[i-1][0]);
if(strcmp(pre2, t1) <= 0) dp[i][0] = min(dp[i][0], dp[i-1][1]);
if(strcmp(pre1, t2) <= 0) dp[i][1] = min(dp[i][1], dp[i-1][0] + c[i]);
if(strcmp(pre2, t2) <= 0) dp[i][1] = min(dp[i][1], dp[i-1][1] + c[i]);
copy(pre1, t1);
copy(pre2, t2);
}
printf("%lld\n", min(dp[n][0], dp[n][1]) == 1e18 ? -1 : min(dp[n][0], dp[n][1]));
return 0;
}
用string写了一遍
可以直接赋值直接比较,可以用加法串联,swap等等
但是要用cin,要解绑才不会效率低
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
string s[MAXN][2];
ll dp[MAXN][2];
int c[MAXN], n;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
_for(i, 1, n) cin >> c[i];
_for(i, 1, n)
{
cin >> s[i][0];
s[i][1] = s[i][0];
reverse(s[i][1].begin(), s[i][1].end());
dp[i][0] = dp[i][1] = 1e18;
if(s[i-1][0] <= s[i][0]) dp[i][0] = min(dp[i][0], dp[i-1][0]);
if(s[i-1][1] <= s[i][0]) dp[i][0] = min(dp[i][0], dp[i-1][1]);
if(s[i-1][0] <= s[i][1]) dp[i][1] = min(dp[i][1], dp[i-1][0] + c[i]);
if(s[i-1][1] <= s[i][1]) dp[i][1] = min(dp[i][1], dp[i-1][1] + c[i]);
}
if(min(dp[n][0], dp[n][1]) == 1e18) cout << "-1" << endl;
else cout << min(dp[n][0], dp[n][1]) << endl;
return 0;
}
poj 3764
想半天不知道图上路径怎么处理
看题解发现要用lca的思想
把路径转化为两个点到根节点,这样就变成上一道题了
这个真忘记了,没办法,有时候一直卡住没有思路是因为需要用到的算法是知识盲区
lca我又已经忘记了
那就明天开始复习lca吧
反正现在就做题,哪里不会补哪里
通过题目来巩固知识点
现在Tire树这个知识点我已经掌握了
感觉还有好多算法我要补
二分图,状压dp,lca,kmp……
慢慢来吧
1.31(LCA)
继续学算法吧 今天复习LCA
买了一本书 信息学奥赛提高篇
下学期可以重点吃透这本书
加油
「一本通 4.4 例 1」点的距离(模板)
学习新算法一定要深刻理解原理,自己推一遍
这样子代码就很容易写出
以后也不会忘记
这个学习方法非常重要
lca无非就是树上倍增
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 + 10;
const int MAXM = 20;
int up[MAXN][MAXM], d[MAXN], n, q;
vector<int> g[MAXN];
void dfs(int u, int fa)
{
for(auto v: g[u])
{
if(v == fa) continue;
d[v] = d[u] + 1;
up[v][0] = u;
dfs(v, u);
}
}
void init()
{
dfs(1, -1);
REP(j, 1, MAXM)
_for(i, 1, n)
up[i][j] = up[up[i][j-1]][j-1];
}
int lca(int a, int b)
{
if(d[a] < d[b]) swap(a, b);
for(int j = MAXM - 1; j >= 0; j--)
if(d[up[a][j]] >= d[b])
a = up[a][j];
if(a == b) return a;
for(int j = MAXM - 1; j >= 0; j--)
if(up[a][j] != up[b][j])
a = up[a][j], b = up[b][j];
return up[a][0];
}
int main()
{
scanf("%d", &n);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
init();
scanf("%d", &q);
while(q--)
{
int u, v;
scanf("%d%d", &u, &v);
printf("%d\n", d[u] + d[v] - 2 * d[lca(u, v)]);
}
return 0;
}
poj 2763(LCA+树状数组+dfs序+差分)
这题像是很多题拼在一起的
比较套路
涉及到挺多知识点,要都很熟练才能做出来
首先是求两点之间距离就要用lca转化到点到根节点的距离
但是这题多了一点,边的长度会变化
也就是每点到根的距离会变化
一次变化就会影响一颗子树的值,一颗子树的每一个值都会加上或者减去一个数
然后每次询问一个点的值
用dfs序把节点拍到一个线性数组上,然后支持动态区间修改,询问单点吗
所以就是树状数组加一维差分。树状数组本来可以支持单点修改,区间查询,用个差分转化一下就可以支持区间修改,单点查询。
我写完后一直卡在一个点上很久
源于我对dfs序运用的不熟练
初始化dfs序后,要操作节点的时候,一定要用它的序号,而不是这个点本身
也就是说要用L[u]而不是u
还是蛮有收获的
复习了挺多知识点
#include<cstdio>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 + 10;
const int MAXM = 20;
int up[MAXN][MAXM], f[MAXN], d[MAXN], D[MAXN];
int L[MAXN], R[MAXN], cnt, n, q, s;
int u[MAXN], v[MAXN], w[MAXN];
struct node
{
int v, w;
node(int v, int w) : v(v), w(w) {}
};
vector<node> g[MAXN];
int lowbit(int x) { return x & (-x); }
void add(int x, int p)
{
for(; x <= n; x += lowbit(x))
f[x] += p;
}
int sum(int x)
{
int res = 0;
for(; x; x -= lowbit(x))
res += f[x];
return res;
}
void Add(int l, int r, int p)
{
add(l, p); add(r + 1, -p);
}
void dfs(int u, int fa)
{
L[u] = ++cnt;
REP(i, 0, g[u].size())
{
node x = g[u][i];
int V = x.v;
if(V == fa) continue;
D[V] = D[u] + x.w;
d[V] = d[u] + 1;
up[V][0] = u;
Add(cnt + 1, cnt + 1, D[V]); //cnt + 1是V的序号。这里区间缩为1个点,照常操作
dfs(V, u);
}
R[u] = cnt;
}
void init()
{
dfs(1, -1);
REP(j, 1, MAXM)
_for(i, 1, n)
up[i][j] = up[up[i][j-1]][j-1];
}
int lca(int a, int b)
{
if(d[a] < d[b]) swap(a, b);
for(int j = MAXM - 1; j >= 0; j--)
if(d[up[a][j]] >= d[b])
a = up[a][j];
if(a == b) return a;
for(int j = MAXM - 1; j >= 0; j--)
if(up[a][j] != up[b][j])
a = up[a][j], b = up[b][j];
return up[a][0];
}
int main()
{
scanf("%d%d%d", &n, &q, &s);
_for(i, 1, n - 1)
{
scanf("%d%d%d", &u[i], &v[i], &w[i]);
g[u[i]].push_back(node(v[i], w[i]));
g[v[i]].push_back(node(u[i], w[i]));
}
init();
while(q--)
{
int op, x, y;
scanf("%d", &op);
if(op == 0)
{
scanf("%d", &x);
printf("%d\n", sum(L[s]) + sum(L[x]) - 2 * sum(L[lca(s, x)])); //用了dfs序列初始化后,后面处理的时候要用L[]而不是用点本身 。血的教训
s = x;
}
else
{
scanf("%d%d", &x, &y);
int t = d[u[x]] > d[v[x]] ? u[x] : v[x];
Add(L[t], R[t], -w[x] + y);
w[x] = y;
}
}
return 0;
}
在做一道lca的题遇到了树上差分
干脆顺便把各种差分都复习一遍
搞起
差分数组 and 树上差分这个写得相当好,我就是看这个学差分的
P1083 [NOIP2012 提高组] 借教室(二分+一维差分)
水题
差分使用于离线的区间修改问题
如果要在线就结合树状数组
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int r[MAXN], d[MAXN], s[MAXN], t[MAXN], n, m;
int a[MAXN];
void add(int l, int r, int w)
{
a[l] += w; a[r + 1] -= w;
}
bool check(int key)
{
_for(i, 1, n) a[i] = r[i] - r[i-1];
_for(i, 1, key) add(s[i], t[i], -d[i]);
_for(i, 1, n)
{
a[i] += a[i-1];
if(a[i] < 0) return false;
}
return true;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%d", &r[i]);
_for(i, 1, m) scanf("%d%d%d", &d[i], &s[i], &t[i]);
int l = 0, r = m + 1;
while(l + 1 < r)
{
int mid = (l + r) >> 1;
if(check(mid)) l = mid;
else r = mid;
}
if(r == m + 1) puts("0");
else printf("-1\n%d\n", r);
return 0;
}
树上差分原理
可以用来解决一个段路径上点或边经过多少次的问题
可以从最后统计方式反推,最后是子树和
点差分的话就是d[u]++ d[v]++ d[lca(u, v)]-- d[fa[lca(u, v)]]--
边差分的话就d[u]++ d[v]++ d[lca(u, v)] -= 2
不是按照一次,而是加减某个值,就可以看作经过很多次,一样的
P3128 [USACO15DEC]Max Flow P(树上差分模板)
模板题
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 5e4 + 10;
const int MAXM = 20;
int up[MAXN][MAXM], d[MAXN], a[MAXN], n, m, ans;
vector<int> g[MAXN];
void dfs(int u, int fa)
{
for(auto v: g[u])
{
if(v == fa) continue;
d[v] = d[u] + 1;
up[v][0] = u;
dfs(v, u);
}
}
void init()
{
dfs(1, 0);
REP(j, 1, MAXM)
_for(i, 1, n)
up[i][j] = up[up[i][j-1]][j-1];
}
int lca(int a, int b)
{
if(d[a] < d[b]) swap(a, b);
for(int j = MAXM - 1; j >= 0; j--)
if(d[up[a][j]] >= d[b])
a = up[a][j];
if(a == b) return a;
for(int j = MAXM - 1; j >= 0; j--)
if(up[a][j] != up[b][j])
a = up[a][j], b = up[b][j];
return up[a][0];
}
void sum(int u, int fa)
{
for(auto v: g[u])
{
if(v == fa) continue;
sum(v, u); //先往下搜,回溯的时候再统计。这里不要写成dfs,写sum
a[u] += a[v];
}
ans = max(ans, a[u]);
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
init();
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
a[u]++; a[v]++; a[lca(u, v)]--; a[up[lca(u, v)][0]]--;
}
sum(1, 0);
printf("%d\n", ans);
return 0;
}
2.1(树上差分)
今天继续复习树上差分
真的隔了高三一年,很多很多之前的东西全都忘记了
现在都捡起来
P3258 [JLOI2014]松鼠的新家(树上点差分+小细节)
竟然发现之前我写的模板是错的
我的那种写法,根节点和根节点的父亲深度是一样的
所以我现在换了一种写法,是对的
注意根节点是1,根节点的父亲是0
这两个深度不同,要小心
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 3e5 + 10;
const int MAXM = 20;
int up[MAXN][MAXM], b[MAXN], a[MAXN], d[MAXN], n;
vector<int> g[MAXN];
void dfs(int u, int fa)
{
up[u][0] = fa; d[u] = d[fa] + 1;
REP(j, 1, MAXM) up[u][j] = up[up[u][j-1]][j-1];
for(auto v: g[u])
{
if(v == fa) continue;
dfs(v, u);
}
}
void sum(int u, int fa)
{
for(auto v: g[u])
{
if(v == fa) continue;
sum(v, u);
a[u] += a[v];
}
}
int lca(int a, int b)
{
if(d[a] < d[b]) swap(a, b);
for(int j = MAXM - 1; j >= 0; j--)
if(d[up[a][j]] >= d[b])
a = up[a][j];
if(a == b) return a;
for(int j = MAXM - 1; j >= 0; j--)
if(up[a][j] != up[b][j])
a = up[a][j], b = up[b][j];
return up[a][0];
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &b[i]);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
_for(i, 2, n)
{
int u = b[i], v = b[i-1];
a[u]++; a[v]++;
a[lca(u, v)]--; a[up[lca(u, v)][0]]--;
}
sum(1, 0);
_for(i, 2, n) a[b[i]]--;
_for(i, 1, n) printf("%d\n", a[i]);
return 0;
}
POJ 3417(思维+树上边差分)
这题主要难在思维上
一开始关注点在主要边,没想出来
后来我看了题解,主要关注点在附加边
切成多个联通分量的关键点在于环,多一条附加边就会成环
其实是我图论题做的少,对这个不熟悉,没有经验
一条附加边就会构成一个环,路径上的每条边要加1
最后如果边值为0,就有m个方案
为1就有1个方案
大于1就不可能
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 + 10;
const int MAXM = 20;
int up[MAXN][MAXM], a[MAXN], d[MAXN], n, m;
vector<int> g[MAXN];
void dfs(int u, int fa)
{
up[u][0] = fa; d[u] = d[fa] + 1;
REP(j, 1, MAXM) up[u][j] = up[up[u][j-1]][j-1];
for(auto v: g[u])
{
if(v == fa) continue;
dfs(v, u);
}
}
void sum(int u, int fa)
{
for(auto v: g[u])
{
if(v == fa) continue;
sum(v, u);
a[u] += a[v];
}
}
int lca(int a, int b)
{
if(d[a] < d[b]) swap(a, b);
for(int j = MAXM - 1; j >= 0; j--)
if(d[up[a][j]] >= d[b])
a = up[a][j];
if(a == b) return a;
for(int j = MAXM - 1; j >= 0; j--)
if(up[a][j] != up[b][j])
a = up[a][j], b = up[b][j];
return up[a][0];
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
_for(i, 1, m)
{
int u, v;
scanf("%d%d", &u, &v);
a[u]++; a[v]++; a[lca(u, v)] -= 2;
}
sum(1, 0);
int ans = 0;
_for(i, 2, n)
{
if(a[i] == 1) ans++;
if(a[i] == 0) ans += m;
}
printf("%d\n", ans);
return 0;
}
P2680 [NOIP2015 提高组] 运输计划(二分+LCA+树上边差分)
独立做出紫题,爽
还是想了蛮久的
首先我们所要求的答案时间显然是单调性的,也就是说这个时间满足,大于这个时间肯定也满足
所以可以用二分答案
那么对于一个给出的时间
就有一些路径是超过这些时间的
那么就求这些路径共同覆盖的边中最大的,然后减去,看能否满足就行
求路径长度要lca,多条路径共同覆盖的边也要用lca树上边差分
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 3e5 + 10;
const int MAXM = 20;
int up[MAXN][MAXM], d[MAXN], D[MAXN], a[MAXN];
int ti[MAXN], n, m, ma;
struct node
{
int u, v, len;
}r[MAXN];
struct node2 { int v, w; };
vector<node2> g[MAXN];
bool cmp(node a, node b)
{
return a.len > b.len;
}
void dfs(int u, int fa)
{
up[u][0] = fa; d[u] = d[fa] + 1;
REP(j, 1, MAXM) up[u][j] = up[up[u][j-1]][j-1];
for(auto x: g[u])
{
if(x.v == fa) continue;
D[x.v] = D[u] + x.w;
ti[x.v] = x.w;
dfs(x.v, u);
}
}
int lca(int a, int b)
{
if(d[a] < d[b]) swap(a, b);
for(int j = MAXM - 1; j >= 0; j--)
if(d[up[a][j]] >= d[b])
a = up[a][j];
if(a == b) return a;
for(int j = MAXM - 1; j >= 0; j--)
if(up[a][j] != up[b][j])
a = up[a][j], b = up[b][j];
return up[a][0];
}
void sum(int u, int fa, int t)
{
for(auto x: g[u])
{
if(x.v == fa) continue;
sum(x.v, u, t);
a[u] += a[x.v];
}
if(a[u] == t - 1 && u != 1) ma = max(ma, ti[u]);
}
bool check(int key)
{
if(key >= r[1].len) return true;
int t = lower_bound(r + 1, r + m + 1, node{0, 0, key}, cmp) - r; //upper和lower区别在于相等时怎么处理,这里要思考一下
memset(a, 0, sizeof(a));
REP(i, 1, t)
{
int u = r[i].u, v = r[i].v;
a[u]++; a[v]++; a[lca(u, v)] -= 2;
}
ma = 0;
sum(1, 0, t);
return r[1].len - ma <= key;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n - 1)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u].push_back(node2{v, w});
g[v].push_back(node2{u, w});
}
dfs(1, 0);
_for(i, 1, m)
{
scanf("%d%d", &r[i].u, &r[i].v);
r[i].len = D[r[i].u] + D[r[i].v] - 2 * D[lca(r[i].u, r[i].v)];
}
sort(r + 1, r + m + 1, cmp);
int l = -1, r = 1e9;
while(l + 1 < r)
{
int mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid;
}
printf("%d\n", r);
return 0;
}
一定要注意学习方法的问题
做题目独立思考,至少思考一个小时
不要轻易看题解和代码
2.2(KMP)
kmp算法还是有点难理解了
我花了一个上午认真看了几篇博客算是彻底理解了,自己能讲出来来龙去脉
P3375 【模板】KMP字符串匹配
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
char a[MAXN], b[MAXN];
int Next[MAXN], lena, lenb;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < lenb)
{
if(j == -1 || b[i] == b[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
void kmp()
{
int i = 0, j = 0;
while(i < lena)
{
if(j == -1 || a[i] == b[j])
{
i++; j++;
if(j == lenb) printf("%d\n", i - j + 1);
}
else j = Next[j];
}
}
int main()
{
scanf("%s%s", a, b);
lena = strlen(a), lenb = strlen(b);
get_next();
kmp();
REP(i, 0, lenb) printf("%d ", Next[i+1]); puts("");
return 0;
}
用next数组可以有一个重复子串的结论
len % (len - next[len +1]) == 0时为重复子串,比如abcabcabc
可以自己用笔画一下
一段长度为len - next[len +1]
CF1200E Compress Words
一开始理解错意思,以为只有前后两个合并
原来是合并的结果和新的一个合并,卡了好久
考的就是kmp的合并过程
学算法一定要深刻理解原理,真正的题目不会是模板,而是变形,那就要靠对算法本身的理解
这道题还是学到一些东西的
(1)string截取。字符串的题目用string很方便,字符长度不定,连接直接加,复制直接赋值
这里学到了截取的操作
s.substr(pos) 表示从pos到结尾的一段
s.substr(pos, n)表示从pos开始,长度为n的一段
(2)kmp的一些细节。这道题要求模式串匹配完了之后继续往后
其实next数组是有求到next[len]的,那样就照常往下匹配好了
加一句j != lenb
(3)交上去超时了。最后匹配的时候只用看最后一段就好了,前面其实是没有用的
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
string a, b;
int Next[MAXN], lena, lenb, n;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < lenb)
{
if(j == -1 || b[i] == b[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
int kmp()
{
get_next();
int i = lena - lenb, j = 0;
while(i < lena)
{
if(j != lenb && (j == -1 || a[i] == b[j])) i++, j++;
else j = Next[j];
}
return j - 1;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> a;
_for(i, 2, n)
{
cin >> b;
lena = a.length(), lenb = b.length();
a += b.substr(kmp() + 1);
}
cout << a << endl;
return 0;
}
P4824 [USACO15FEB]Censoring S(分类讨论)
开始用kmp暴力删除元素。同时主串不用从头开始匹配,有些地方已经匹配过不可能了。交上去T了2个点,主要是子串很小的时候要删除很多次,一次删除是O(n)
于是我就在子串比较小的时候写了另外一种算法,用一个栈暴力,这个时候就是之前校赛的一道题了,所以我很快就想到了方法,子串很小的时候可以这么做
交上去AC了
不过我觉得这应该不是正解,我这是分两种情况写的
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int Next[MAXN], lena, lenb, st;
string a, b;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < lenb)
{
if(j == -1 || b[i] == b[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
bool kmp()
{
int i = st, j = 0;
while(i < lena && j < lenb)
{
if(j == -1 || a[i] == b[j]) i++, j++;
else j = Next[j];
}
if(j == lenb)
{
a = a.substr(0, i - j) + a.substr(i);
st = max(i - j - lenb + 1, 0);
lena -= lenb;
return true;
}
return false;
}
char s[MAXN];
int p;
bool check()
{
if(p < lenb) return false;
_for(i, p - lenb + 1, p)
if(s[i] != b[i - (p - lenb + 1)])
return false;
return true;
}
void solve()
{
REP(i, 0, lena)
{
s[++p] = a[i];
if(check()) p -= lenb;
}
_for(i, 1, p) cout << s[i];
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> a >> b;
lena = a.length(), lenb = b.length();
st = 0;
if(lenb <= 100)
{
solve();
return 0;
}
get_next();
while(kmp());
cout << a << endl;
return 0;
}
瞟了一眼正解,是结合我两种方法的优点,也就是kmp+栈
明天写写吧。
2.3(KMP)
P4824 [USACO15FEB]Censoring S(kmp+栈)
利用kmp滑动模式串的性质,当一个匹配成功后,不用从头开始
要对kmp有深刻的理解
我发现一开始求next数组时j=-1是为了前后缀不等于整个串。j一直会比i小,因为i,j同时加,j有时还会等于next[j]而倒推
如果i=j的话就是全串了。总之一开始设j为-1,然后改该怎么跑怎么跑
这道题关键就是模仿next数组求一个数组可以往前滑动
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int Next[MAXN], k[MAXN], lena, lenb, p, sti, stj;
string a, b;
char s[MAXN];
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < lenb)
{
if(j == -1 || b[i] == b[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
void kmp()
{
int i = sti, j = stj;
while(i <= p && j < lenb)
{
if(j == -1 || s[i] == b[j])
{
k[i] = j;
i++, j++;
}
else j = Next[j];
}
sti = i, stj = j;
if(j == lenb)
{
p -= lenb;
sti = i - j;
stj = k[i - j - 1] + 1;
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> a >> b;
lena = a.length(), lenb = b.length();
sti = 1; stj = 0;
get_next();
REP(i, 0, lena)
{
s[++p] = a[i];
kmp();
}
_for(i, 1, p) cout << s[i];
return 0;
}
这个题单很不错耶
立个flag,下个学期刷完这份题单。大一下的目标就是这个
做到大部分题目都独立做出
「一本通 2.2 例 1」剪花布条(kmp细节修改)
注意统计的是不重叠的子串。所以每次结束后要j设为0
深刻理解kmp,修改一些细节即可
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
int Next[MAXN], lena, lenb;
string a, b;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < lenb)
{
if(j == -1 || b[i] == b[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
int work()
{
lena = a.length(), lenb = b.length();
get_next();
int i = 0, j = 0, res = 0;
while(i < lena)
{
if(j == -1 || a[i] == b[j]) i++, j++;
else j = Next[j];
if(j == lenb) res++, j = 0;
}
return res;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
while(1)
{
cin >> a;
if(a[0] == '#' && a.length() == 1) break;
cin >> b;
cout << work() << endl;
}
return 0;
}
「一本通 2.1 练习 1」Power Strings(重复子串结论)
用到了前面说的那个结论
注意一定要能整除才行
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int Next[MAXN], lena;
string a;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < lena)
{
if(j == -1 || a[i] == a[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
while(1)
{
cin >> a;
if(a == ".") break;
lena = a.length(); get_next();
if(lena % (lena - Next[lena]) == 0) cout << lena / (lena - Next[lena]) << endl;
else cout << "1" << endl;
}
return 0;
}
「一本通 2.2 练习 1」Radio Transmission(重复子串变形)
和上一道题类似。不同的是重复的部分会多出来前面的一部分。这个时候就把这一部分删掉就行。
枚举删掉的部分就好
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int Next[MAXN], len;
string a;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < len)
{
if(j == -1 || a[i] == a[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> len >> a;
get_next();
for(int i = len; i >= 1; i--)
if(i % (i - Next[i]) == 0)
{
cout << i - Next[i] << endl;
break;
}
return 0;
}
2.4(KMP)
11点睡6点起+晨跑半小时,即使前一天很晚睡依然6点起
这个习惯真的太好了,每一天都精神饱满。
最近几天我训练量都挺大的,精力来源于这个习惯
这个寒假一定要坚持下去这个习惯,不管几点睡都第二天6点起+晨跑半小时。尽量11点前睡
今天看到了一篇文章
总结几点
一.关于算法的学习。
算法一定要理解本质,来龙去脉。我之前有一个问题,就是一个算法学了一段时间之后就敲不出模板了
而最近我发现,如果我深刻理解了算法,能自己讲出来,把别人讲懂
那么即使过了很长时间,我依然记得算法的本质,就可以写出完整的代码
甚至写出来的代码每次都不完全一样,但都写出来了
实际做题很多时候是模板的变形,考你对算法本质的理解
我最近做kmp的题,就很多考的是kmp的思想。你需要深刻理解kmp才能写出来,只记模板是写不出来的
所以以后学算法一定要深刻理解本质,这样才不会遗忘,也能做出变形的题目
二.独立思考
这也强化了我之前的想法。我的OI生涯之所以结果不好,最重要的原因就是独立思考少,看题解太多,抄代码太多
这样麻痹自己好像学到了很多东西,其实效率很低
宁愿做题慢一些,想久一些,不深刻想一段时间绝不看题解
即使看题解也是理解思路,不会去看代码
三.热爱
内在驱动力才能走得更远
保持对acm的热爱,对解题的热爱,快乐地度过我的acm生涯
#10046. 「一本通 2.2 练习 2」OKR-Periods of Words(kmp变形)
这题是个好题,要深刻理解kmp才能做出
读题后发现需要一个数组,即前缀和后缀相等的最小长度
求出这个数组就完事
这个数组可以用next数组去求,用笔画一画。
一开始写完了T了两个点,后面记忆化了一下就ac了
好题好题
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXN = 1e6 + 10;
int Next[MAXN], k[MAXN], len;
string a;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < len)
{
if(j == -1 || a[i] == a[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
int dfs(int x)
{
if(k[x]) return k[x];
if(Next[x]) return k[x] = dfs(Next[x]);
else return x;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> len >> a;
get_next();
ll ans = 0;
_for(i, 1, len)
{
int t = dfs(i);
if(t <= i / 2) ans += i - t;
}
cout << ans << endl;
return 0;
}
「一本通 2.2 练习 3」似乎在梦中见过的样子(kmp变形)
想了个O(n^2)的算法,这个时间复杂度对于1.5e4来说很极限,想试一下能不能过,然后竟然过了,最慢0.4s
看来1e7到1e8都能过
其实思路并不难
首先是枚举每一个位置,即枚举左端点,我的实现方法是每次删除第一个字符来
对于当前字符串,做一个kmp
然后枚举右端点,就有一个子串
这道题也是用了Next数组的性质,跟上一道有点像
这里贪心一下,求出前后缀相同长度在大于等于k的情况下最小
就一直对本身Next就好,因为会重复计算,所以要记忆化一下
求出来后按照题目要求写个判断就好
至此,题单里面的kmp就全部刷完了,爽!!都是独立思考做出的!!
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1.5e4 + 10;
int Next[MAXN], f[MAXN], k, st, len, ans;
string a;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < len)
{
if(j == -1 || a[i] == a[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
int dfs(int x)
{
if(f[x]) return f[x];
if(Next[x] >= k) return f[x] = dfs(Next[x]);
else return x;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> a >> k;
while(1)
{
memset(f, 0, sizeof(f));
len = a.length();
get_next();
_for(i, 1, len)
{
int t = dfs(i);
if(t >= k && i >= 2 * t + 1) ans++;
}
if(len == 1) break;
a = a.substr(1);
}
cout << ans << endl;
return 0;
}
明天复习二分图
2.5(二分图)
首先学习二分图最大匹配,匈牙利算法
二分图最大匹配——匈牙利算法这个讲得很好
其实蛮简单的,本质就是一直递归下去看能不能腾出位置,这样就遍历了所有可能
比如a被占了,那么看占a的人能不能占其他的,同时a就在后面不能再访问了,那么占a的人又去找,这就是一样的过程,就是递归了
一直这样下去,如果找到了一个没匹配的,那么连锁反应前面所有都可以有新的匹配了
理解算法本质之后自己写出代码,不要去抄模板
记住理解思路后自己写代码,千万千万不要去抄代码
HDU 2063(二分图最大匹配模板)
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 500 + 10;
int vis[MAXN], link[MAXN], k, m, n;
vector<int> g[MAXN];
bool dfs(int u)
{
for(auto v: g[u])
{
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
while(scanf("%d", &k) && k)
{
scanf("%d%d", &m, &n);
memset(link, 0, sizeof(link));
_for(i, 1, m) g[i].clear();
while(k--)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
}
int ans = 0;
_for(i, 1, m)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", ans);
}
return 0;
}
poj 2492(二分图判定染色法)
染色法挺简单的,一边为0,一边为1 如果遇到同种颜色就不是二分图
要理解思想,不要抄代码
理解思想后自己写代码实现
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 2000 + 10;
int color[MAXN], vis[MAXN], n, m;
vector<int> g[MAXN];
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v])
{
if(color[u] == color[v]) return false;
continue;
}
vis[v] = 1;
color[v] = color[u] ^ 1;
if(!dfs(v)) return false;
}
return true;
}
bool check()
{
_for(i, 1, n)
if(!vis[i] && !dfs(i))
return false;
return true;
}
int main()
{
int T; scanf("%d", &T);
_for(kase, 1, T)
{
scanf("%d%d", &n, &m);
_for(i, 1, n) g[i].clear();
memset(color, 0, sizeof(color));
memset(vis, 0, sizeof(vis));
while(m--)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
if(!check()) printf("Scenario #%d:\nSuspicious bugs found!\n\n", kase);
else printf("Scenario #%d:\nNo suspicious bugs found!\n\n", kase);
}
return 0;
}
poj 3041(最小覆盖点集=最大匹配)
这里用到了一个结论
最小覆盖点集=最大匹配
最小覆盖点集就是用选择最少的点,使得所有的边至少有一个端点被选中
即用最少的点去覆盖所有边
对于这道题,需要建图
(x, y)在x与y连一条边,二分图左边是行,右边是列
那就求最小覆盖点集就行了
这种坐标x与y坐标连接建立图我之前遇到过
这里的y不用加上n
记住算法原理,每次做题都重新根据原理写一遍,不要copy模板
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 500 + 10;
int link[MAXN], vis[MAXN], n, k;
vector<int> g[MAXN];
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &k);
while(k--)
{
int x, y;
scanf("%d%d", &x, &y);
g[x].push_back(y);
}
int ans = 0;
_for(i, 1, n)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", ans);
return 0;
}
poj 1325(最小覆盖点集)
匈牙利算法的时间复杂度是nm,点数乘边数
这道题可以转化成上一道题,一个任务看成一个点,一种模式看作可以干掉一行或者一列
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 100 + 10;
int link[MAXN], vis[MAXN], n, m, k;
vector<int> g[MAXN];
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
while(scanf("%d", &n) && n)
{
memset(link, 0, sizeof(link));
_for(i, 0, n - 1) g[i].clear();
scanf("%d%d", &m, &k);
while(k--)
{
int i, x, y;
scanf("%d%d%d", &i, &x, &y);
if(!x || !y) continue;
g[x].push_back(y);
}
int ans = 0;
_for(i, 1, n - 1)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", ans);
}
return 0;
}
poj 2226(最小覆盖点集+建图)
这题和3041很像
不同的是那道题一下可以干掉一行或者一列,这道题只能干掉连续的一行或一列
那就转化一下就好了,把这种情况区分一下
所以就给他们分配id就好了
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 60;
int link[MAXN * MAXN], vis[MAXN * MAXN], n, m;
int x[MAXN][MAXN], y[MAXN][MAXN], idx, idy;
vector<int> g[MAXN * MAXN];
char s[MAXN][MAXN];
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%s", s[i] + 1);
_for(i, 1, n)
_for(j, 1, m)
if(s[i][j] == '*')
{
int p = j; idx++;
while(s[i][p + 1] == '*') p++;
_for(k, j, p) x[i][k] = idx;
j = p;
}
_for(j, 1, m)
_for(i, 1, n)
if(s[i][j] == '*')
{
int p = i; idy++;
while(s[p + 1][j] == '*') p++;
_for(k, i, p) y[k][j] = idy;
i = p;
}
_for(i, 1, n)
_for(j, 1, m)
if(s[i][j] == '*')
g[x[i][j]].push_back(y[i][j]);
int ans = 0;
_for(i, 1, idx)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", ans);
return 0;
}
2.6(二分图)
最近有点疲惫,毕竟连续刚了这多天了
所以这几天做题量会少一些
poj 2724(最大匹配+建图)
这题真的做了我好久
怎么建图是关键
很容易想到只差一位的就连一条边
建立二分图的关键在于分成两部分,同一部分之间没有边相连
就是说同一部分不可能只差一位
如果只差一位,那么1的奇偶是不同的。也就是说,1的奇偶相同的时候不可能有边
所以可以这样把其分类
这样就可以把他们根据1的奇偶扔到两个数组,然后再连边
还有一种实现起来更简单的方法
就是两部分都是自己。因为自己和自己不可能连边
那么这个时候边也要翻倍了,也就是一个对称的图。
那么最大匹配也会翻倍,最后除以2就好了
然后提一下为什么是最大匹配
首先题目说不能覆盖,也就是不可能两条边连在一个点上。而二分图的匹配就是左边的一个点匹配右边的一个点,此外没有边连他们,符合这个特质
然后多一条边就可以少一次净化,所以边要最多,就是最大匹配
然后一定要注意判重,太坑了。判重可以用set,但是要用string,交到poj会编译错误
判重离散化都可以用set,挺方便的
#include<cstring>
#include<vector>
#include<cstdio>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
int link[MAXN << 1], vis[MAXN << 1], n, m, cnt;
char s[MAXN << 1][20], str[MAXN << 1][20];
vector<int> g[MAXN << 1];
bool check(char a[], char b[])
{
int num = 0;
REP(i, 0, n)
num += a[i] != b[i];
return num == 1;
}
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
void add(char a[])
{
_for(i, 1, cnt)
if(!strcmp(s[i], a))
return;
strcpy(s[++cnt], a);
}
int main()
{
while(scanf("%d%d", &n, &m) && n)
{
memset(link, 0, sizeof(link));
memset(s, 0, sizeof(s));
cnt = 0;
_for(i, 1, m)
{
scanf("%s", str[i]);
REP(j, 0, n)
{
if(str[i][j] == '*')
{
str[i][j] = '0'; add(str[i]);
str[i][j] = '1'; add(str[i]);
break;
}
if(j == n - 1) add(str[i]);
}
}
m = cnt;
_for(i, 1, m) g[i].clear();
_for(i, 1, m)
_for(j, i + 1, m)
if(check(s[i], s[j]))
{
g[i].push_back(j);
g[j].push_back(i);
}
int ans = 0;
_for(i, 1, m)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", m - ans / 2);
}
return 0;
}
2.8(二分图)
昨天休息,去了书城
感觉恢复的差不多了
坚持早睡早起,晨跑
搞起
二分图进展有点慢,因为题目的建模我要想挺久
那就慢一点吧
poj 3020(最小路径覆盖)
这题做了n久
卡在怎么建图
我受前面题目的影响,一直在往最小点覆盖方向想
这样的话一个点就是一条边
而这道题不一样,它是一个点就是二分图中的节点,而连接方式是边
节点倍长一下,对称,最后答案除以2
最小路径覆盖指的的是用最少的路径来覆盖所有节点
最小路径覆盖 = 总节点 - 最大匹配
这里我想到了如果有些节点没有边怎么办
从公式来看,这些多余的点就多在总节点这个地方,最小路径覆盖数就会加
刚好是符合题意的
然后这里连边的时候注意越界的问题,越界就return保险一点
我因为没有return,连到了之前输入的图的点了
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 50;
const int MAXM = 410;
int id[MAXN][MAXN], link[MAXM], vis[MAXM], n, m, cnt;
char s[MAXN][MAXN];
vector<int> g[MAXM];
void build(int x, int y, int xx, int yy)
{
if(s[xx][yy] != '*') return;
g[id[x][y]].push_back(id[xx][yy]);
}
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
memset(link, 0, sizeof(link));
memset(id, 0, sizeof(id));
memset(s, 0, sizeof(s));
cnt = 0;
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%s", s[i] + 1);
_for(i, 1, n)
_for(j, 1, m)
if(s[i][j] == '*')
{
id[i][j] = ++cnt;
g[cnt].clear();
}
_for(i, 1, n)
_for(j, 1, m)
if(s[i][j] == '*')
{
build(i, j, i - 1, j);
build(i, j, i + 1, j);
build(i, j, i, j - 1);
build(i, j, i, j + 1);
}
int ans = 0;
_for(i, 1, cnt)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", (cnt * 2 - ans) / 2);
}
return 0;
}
poj 1466(最大独立集)
选一些点,这些点之间两两没有边相连,为独立集
点最多为最大独立集
记住一个定理
最小点覆盖+最大独立集 = 总节点
除去最小点覆盖的点,剩下的点就是最大独立集
如果剩下点有边相连,那么最小点覆盖就没有覆盖完,矛盾
反证法挺有用的
点覆盖最小,独立集就最大
所以最大独立集=总节点-最大匹配
这道题是二分图对称的,所以答案除以2
这道题有个坑点就是从0开始
而匈牙利算法0是默认没有点相连的
所以输入的时候+1处理一下
以后遇到标号从0开始都可以处理一下成从1开始
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 510;
int link[MAXN], vis[MAXN], n;
vector<int> g[MAXN];
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
while(~scanf("%d", &n))
{
memset(link, 0, sizeof(link));
_for(i, 1, n) g[i].clear();
_for(i, 1, n)
{
int x, m;
scanf("%d: (%d)", &x, &m);
x++;
while(m--)
{
int y; scanf("%d", &y);
g[x].push_back(++y);
}
}
int ans = 0;
_for(i, 1, n)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", (n * 2 - ans) / 2);
}
return 0;
}
poj 3692(最大独立集+建图)
这道题要在二分图中求n个点,任意属于两个集合中的点都有边相连
而独立集恰好是任意两个集合的点都没有边相连
所以我们建图可以反过来
把不存在的边连上,存在的边删除
然后最大独立集就是答案
至此二分图部分结束了,最大匹配,最小点覆盖,最小边覆盖,最大独立集都过了一遍
还是蛮有收获的
明天开启数学问题
加油
#include<cstdio>
#include<vector>
#include<cstring>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 210;
int link[MAXN], vis[MAXN], a[MAXN][MAXN], n, m, k;
vector<int> g[MAXN];
bool dfs(int u)
{
REP(i, 0, g[u].size())
{
int v = g[u][i];
if(vis[v]) continue;
vis[v] = 1;
if(!link[v] || dfs(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
int main()
{
int kase = 0;
while(scanf("%d%d%d", &n, &m, &k) && n)
{
memset(link, 0, sizeof(link));
memset(a, 0, sizeof(a));
_for(i, 1, n) g[i].clear();
while(k--)
{
int u, v;
scanf("%d%d", &u, &v);
a[u][v] = 1;
}
_for(i, 1, n)
_for(j, 1, m)
if(!a[i][j])
g[i].push_back(j);
int ans = 0;
_for(i, 1, n)
{
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("Case %d: %d\n", ++kase, n + m - ans);
}
return 0;
}
2.9(快速幂+质数)
因为22号回去,所以最后一个专题就是数学了
打算搞一波数学,然后留两天敲敲模板就好了
搜索,动态规划,图论,字符串这些我都有一定的知识储备了
但是在数学这部分比较薄弱
搞起
「一本通 6.1 练习 3」越狱(快速幂+细节)
先来一道快速幂热身
快速幂理解本质,代码就不会写错了,不要背模板
然后是一些数学题目要注意的细节
一个是MOD,可以先写好函数,这样就不会出错了
想乘要转long long
相减可能为负,要加MOD
然后这个MOD的话,a的b次方,a可以先MOD,b不能MOD,它是指数
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;
const int MOD = 100003;
typedef long long ll;
int add(int a, int b) { return (a + b) % MOD; }
int mul(int a, int b) { return 1ll * a * b % MOD; }
int binpow(int a, ll b)
{
int res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = mul(res, a);
a = mul(a, a);
}
return res;
}
int main()
{
int m; ll n;
scanf("%d%lld", &m, &n);
m %= MOD;
int ans = binpow(m, n) - mul(m, binpow(m - 1, n - 1));
if(ans < 0) ans += MOD;
printf("%d\n", ans);
return 0;
}
欧拉筛
还是那样,学算法要深刻理解算法,这样一来一些拓展题做得出来,二来代码可以精确根据思想写出来
我尝试自己从头到尾讲一遍线性筛法
埃式筛法的话,核心思想是质数的倍数是合数,那么就用质数去筛掉。
问题在于一个数会被筛多次,比如12 = 2x6 = 3x4
12既被质数2筛了一次,又被质数3筛了一次
那么欧拉筛就是使得每个数中被其最小质因子筛一次,从而优化
那么这么做呢
首先我们枚举每一个数,是质数就标记,加入数组,这是常规操作,和埃式筛法一样
然后我们把当前的数当作倍数,去乘已经得到的质数,从而去筛
那么关键是保证这个质因子一定是最小的
核心操作是当前数是当前质数的倍数的时候,就break,停止
为什么呢
假设当前数为i,质数为p,那么i % p == 0,即i可以表示为np时
如果不break掉,就继续到下一个质数,假设是(p + k)
那么新的数就是np乘上(p+k),我们是默认(p+k)为np乘上(p+k)的最小质因子
然而不是,因为np乘上(p+k)等价于n(p+k)乘上p,显然存在p为更小的质因子
所以从这往后都存在更小的质因子p,所以就break了
所以一直到i % p == 0的时候就停止就好了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 100 + 10;
bool vis[MAXN];
vector<int> p;
void get_prime()
{
vis[0] = vis[1] = true;
REP(i, 2, MAXN)
{
if(!vis[i]) p.push_back(i);
REP(j, 0, p.size())
{
if(i * p[j] >= MAXN) break;
vis[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
int main()
{
get_prime();
REP(i, 0, p.size()) printf("%d ", p[i]);
return 0;
}
质因数分解
用2到根号n的数试除法就ok了
当然可以优化,比如预处理质数,用质数去试除
更优化的可以用最小质因子去除
void work(int n)
{
for(int i = 2; i * i <= n; i++)
{
if(n % i == 0)
{
p[++cnt] = i;
while(n % i == 0)
{
n /= i;
c[cnt]++;
}
}
}
if(n > 1) p[++cnt] = n, c[cnt]++;
}
「一本通 6.2 例 1」Prime Distance(筛法思想应用)
这题写了好久,因为有坑,我调半天没调调出来
这题应用了筛法的思想,即质数的倍数是合数,用质数去筛
那么我们就去筛l到r这个区间
那么首先需要初始化质数
那么初始化1到根号r就行了
因为比如n = a * b,a或b有一个是小于等于根号n的
我用的线性筛去筛了一下
然后枚举质数,去筛这个区间
标记筛过可以用unorered_map 也可以用数组,下标减去就好
数组时间空间上都更优秀,unorered_map写起来方便一些简洁一些
然后这里有个巨坑
就是有一组数据r为int的最大值
所以for的时候会溢出,变成负数,然后就死循环超时了
太坑了
还有0 1为合数,要特判一下
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5;
unordered_map<int, bool> v;
vector<int> p, ve;
bool vis[MAXN];
void get_prime()
{
vis[0] = vis[1] = true;
REP(i, 2, MAXN)
{
if(!vis[i]) p.push_back(i);
REP(j, 0, p.size())
{
if(i * p[j] >= MAXN) break;
vis[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
int main()
{
get_prime();
int l, r;
while(~scanf("%d%d", &l, &r))
{
v.clear();
REP(j, 0, p.size())
{
if(1ll * p[j] * p[j] > r) break;
_for(i, max(l / p[j], 2), r / p[j])
v[i * p[j]] = true;
}
ve.clear();
_for(i, l, r)
{
if(i < 0) break;
if(!v[i] && i != 1) ve.push_back(i);
}
if(ve.size() <= 1) puts("There are no adjacent primes.");
else
{
int mi = 1e9, t1, t2, ma = 0, t3, t4;
REP(i, 1, ve.size())
{
if(mi > ve[i] - ve[i-1])
{
mi = ve[i] - ve[i-1];
t1 = ve[i-1], t2 = ve[i];
}
if(ma < ve[i] - ve[i-1])
{
ma = ve[i] - ve[i-1];
t3 = ve[i-1], t4 = ve[i];
}
}
printf("%d,%d are closest, %d,%d are most distant.\n", t1, t2, t3, t4);
}
}
return 0;
}
「一本通 6.2 练习 4」Sherlock and His Girlfriend(素数筛)
水题,按照质数合数分两份就行了,是一个二分图
注意特判一下只有质数的情况
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 + 10;
bool vis[MAXN];
vector<int> p;
void get_prime()
{
vis[0] = vis[1] = true;
REP(i, 2, MAXN)
{
if(!vis[i]) p.push_back(i);
REP(j, 0, p.size())
{
if(i * p[j] >= MAXN) break;
vis[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
int main()
{
get_prime();
int n; scanf("%d", &n);
if(n <= 2)
{
if(n == 1) printf("1\n1\n");
if(n == 2) printf("1\n1 1\n");
return 0;
}
puts("2");
_for(i, 1, n)
{
if(vis[i+1]) printf("1 ");
else printf("2 ");
}
puts("");
return 0;
}
「一本通 6.2 练习 2」轻拍牛头(筛法思想+小优化)
正常思路去枚举是n方
这里利用筛法的思想
把每个数去筛
这里会T掉一个点,这里把重复的合并在一起就可以ac了
我没想到这个优化这么明显。其实也只能这么优化了,没有什么其他办法了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
int a[MAXN], v[MAXN], b[MAXN], cnt[MAXN], n, ma;
int main()
{
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%d", &a[i]);
ma = max(ma, a[i]);
cnt[a[i]]++;
}
int num = 0;
_for(i, 1, ma)
if(cnt[i])
b[++num] = i;
_for(i, 1, num)
for(int j = b[i]; j <= ma; j += b[i])
v[j] += cnt[b[i]];
_for(i, 1, n) printf("%d\n", v[a[i]] - 1);
return 0;
}
2.10(质数+约数)
「一本通 6.2 练习 5」樱花(转化式子+求因子个数)
这道题我卡在了这么转化题目那个式子的那一步
然后我后来不小心看到了可以把y化成n!+ t
这一步挺秀的,不知道怎么想出来的
然后转化后就可以发现其实是求n!的平方的因子个数
上次有道题也是一个式子最后转化成求因子个数的。所以以后打表找规律的时候可以往这个方向想,看符不符合
求因子个数可以质因数分解,然后答案就是(cnt[1] + 1) (cnt[2]+1)……
这里有两种做法
一种是直接暴力枚举1到n,对于每一个数去做质因数分解
当然试除法效率很低
更优的方法是预处理出根号n的质数,直接用质数去试除
还有更优的,也就是上次我在cf做到一道题就这么写
就是在欧拉筛的时候存一下每个数的最小质因子,然后用最小质因子去试除,这样是非常非常快的
也就是要预处理1到n的质数
还有第二种做法,就是枚举每一个质数p,个数就是n / p + n / (p * p) + n / (p * p * p)……
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e6 + 10;
const int MOD = 1e9 + 7;
int m[MAXN], cnt[MAXN];
bool vis[MAXN];
vector<int> p;
void get_prime()
{
vis[0] = vis[1] = true;
REP(i, 2, MAXN)
{
if(!vis[i]) p.push_back(i), m[i] = i;
REP(j, 0, p.size())
{
if(i * p[j] >= MAXN) break;
vis[i * p[j]] = true;
m[i * p[j]] = p[j];
if(i % p[j] == 0) break;
}
}
}
void deal(int n)
{
while(n != 1)
{
int t = m[n];
while(n % t == 0)
{
n /= t;
cnt[t] += 2;
}
}
}
int main()
{
get_prime();
int n; scanf("%d", &n);
_for(i, 1, n) deal(i);
int ans = 1;
REP(i, 1, MAXN)
if(cnt[i])
ans = 1ll * ans * (cnt[i] + 1) % MOD;
printf("%d\n", ans);
return 0;
}
求1~N所有数的正约数集合
用试除法n根号n比较慢
这里用倍数法,复杂度为n + n / 2 + n / 3……=nlogn
vector<int> factor[MAXN];
REP(i, 1, MAXN)
for(int j = i; j < MAXN; j += i)
factor[j].push_back(i);
「一本通 6.3 例 1」反素数 Antiprime(观察+搜索)
首先要推这种数有什么特点
因为涉及到因子个数,所以马上联想到质因数分解
可以发现这种数的质因子一定是连续的质数
如果不是,那一定可以找到因子个数相同但是更小的数
所以这种数一定是由前10个质数组成,前10个质数相乘已经快爆int了
所以就可以搜索,枚举每一个质数的幂次
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int p[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int n, ans, t;
void dfs(int now, int i, int num)
{
if(i > 9) return;
if(num > t)
{
t = num;
ans = now;
}
if(num == t) ans = min(ans, now);
int cnt = 0;
while(1ll * now * p[i] <= n)
{
cnt++;
now *= p[i];
dfs(now, i + 1, num * (cnt + 1));
}
}
int main()
{
scanf("%d", &n);
dfs(1, 0, 1);
printf("%d\n", ans);
return 0;
}
最大公约数gcd
了解一些定理
gcd(a, b) = gcd(a-b, b)
因为对于任意一个公约数d
d|a d|b 可得d|(a-b)
同时推出gcd(a%b, b)减很多次就是模
根据gcd(a, b) = gcd(a-b, b)和奇偶可以求大整数的GCD
「一本通 6.3 练习 1」X-factor Chain(质因数分解+排列组合)
质因数分解一下就好
序列个数相当于n个数的排列,有一些重复,把重复的除掉就好
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
vector<int> ve;
int cnt[20], n, num;
ll f[30];
int main()
{
f[0] = f[1] = 1;
_for(i, 2, 20) f[i] = f[i-1] * i;
while(~scanf("%d", &n))
{
ve.clear();
memset(cnt, 0, sizeof(cnt));
num = 0;
for(int i = 2; i * i <= n; i++)
if(n % i == 0)
{
ve.push_back(i); num++;
while(n % i == 0)
{
n /= i;
cnt[num]++;
}
}
if(n > 1) ve.push_back(n), cnt[++num] = 1;
int sum = 0;
_for(i, 1, num) sum += cnt[i];
ll ans = f[sum];
_for(i, 1, num) ans /= f[cnt[i]];
printf("%d %lld\n", sum, ans);
}
return 0;
}
2.13(约数+同余)
前两天过年就没训练了
今天回归训练
这两天应该很开心,但我却遭遇了挫折,挺伤心的
但正如我最近读的斯多葛哲学
控制能控制的,不能控制的努力释怀。事情已经发生,无法控制,无法挽回
但我能控制我自己的态度,是垂头丧气一蹶不振还是找回斗志重新奋斗
同时对已经发生的事情采用宿命论的态度,我就把它当做我的命运吧
一个优秀的人应该迎接命运给它的所有安排。无论发生什么,都是为了最后有个最好的结果
不管怎么样,提升自己,从编程,从英语,从外表,从身体健康,从读书,才是最有价值的事情
「JLOI2014」聪明的燕姿(数论+搜索)
这题挺难,我独立想没有想出来
后来发现是一道紫题,是一次省选里的题目,难怪呢
首先知道一个公式
S = (1 + p1 + p1 * p1……)(1 + p2 + p2 * p2……)……
其中p1,p2是x的质因子,每一个项一直加到最高次数
这个挺好理解的,这样相乘能乘出x的每一个因子
其实我想到这个了,但不知道接下来怎么做了
其实就是搜索,我一直往数学方向想,我可能太少做这种数论+搜索的题目了,没有往搜索这个方向想
搜索可以看作较为优秀的暴力,因为很多无用状态可以排除
我当时一直想把S质因数分解。其实是反过来,枚举因子看能不能除以S。
很多题目反过来想就对了,我要牢记这一点
以后当我做题卡壳的时候,就要告诉自己也许反过来想就有思路了
我们可以枚举每一个质因子p,然后枚举和,看和是不是S的因子
这里的S最大到10的9次方
所以有些p可能非常大,1+p为因子
那么我们这里就分类讨论
先看简单的一类
对于一些1+p+p*p……的,也就是 不是1+p的
我们可以预处理出根号S内的质因数,线性筛一些,枚举就完事
对于1+p的
我们要判断p是否为质数,而且没有与之前的重复,如果可以的话就加入答案
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5;
bool vis[MAXN];
vector<int> p, ans;
int n;
void get_prime()
{
vis[0] = vis[1] = true;
REP(i, 2, MAXN)
{
if(!vis[i]) p.push_back(i);
REP(j, 0, p.size())
{
if(i * p[j] >= MAXN) break;
vis[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
bool check(int num)
{
if(num < MAXN) return !vis[num];
REP(j, 0, p.size())
{
if(1ll * p[j] * p[j] > num) return true;
if(num % p[j] == 0) return false;
}
}
void dfs(int k, int now, int num)
{
if(num == 1) { ans.push_back(now); return; }
if((k == -1 || num - 1 > p[k]) && check(num - 1)) ans.push_back(now * (num - 1));
REP(j, k + 1, p.size())
{
if(1ll * p[j] * p[j] > num) break;
for(long long sum = 1 + p[j], t = p[j]; sum <= num; t *= p[j], sum += t)
if(num % sum == 0)
dfs(j, now * t, num / sum);
}
}
int main()
{
get_prime();
while(~scanf("%d", &n))
{
ans.clear();
dfs(-1, 1, n);
sort(ans.begin(), ans.end());
printf("%d\n", ans.size());
REP(i, 0, ans.size()) printf("%d ", ans[i]);
if(ans.size()) puts("");
}
return 0;
}
扩展欧几里得解(解ax+by=c的不定方程)
void exgcd(int a, int b, int& d, int& x, int& y)
{
if(!b) { d = a; x = 1; y = 0; }
else { exgcd(b, a % b, d, y, x); y -= x * (a / b) ; } //y -= x * (a / b) 要记忆
}
用来解ax+by=c的不定方程
其实用手算一下就可以推出来的
上面代码是求ax+by = gcd(a, b)的一组解
有几个结论
对于ax + by = c
如果gcd(a, b) | c有解,否则无解(整数解)
有解时同乘个系数即可
通解为x = x0 + k * b/gcd
y = y0 - k * a/gcd
「一本通 6.4 例 1」青蛙的约会(扩展欧几里得+细节)
这里有几个细节要注意
(1)数论题最好开long long.我因为没开long long WA了一个点
(2)结论要记好。d | c时有解,得出x与y时要乘一个系数回去。通解公式要记得,注意复数取模
(3)ax+by=c,a和b都要为正,c可以为负。因为要求最大公因数,a和b都要是正数
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
void exgcd(ll a, ll b, ll& d, ll& x, ll& y)
{
if(!b) { d = a; x = 1; y = 0; }
else { exgcd(b, a % b, d, y, x); y -= x * (a / b); }
}
int main()
{
ll x0, y0, m, n, L;
ll x, y, d;
scanf("%lld%lld%lld%lld%lld", &x0, &y0, &m, &n, &L);
if(m - n < 0) swap(m, n), swap(x0, y0);
exgcd(m - n, L, d, x, y);
if((y0 - x0) % d != 0) puts("Impossible");
else
{
x *= (y0 - x0) / d, y *= (y0 - x0) / d;
ll mod = L / d;
x %= mod;
if(x < 0) x += mod;
printf("%lld\n", x);
}
return 0;
}
线性同余方程
ax = b(mod m),解x为同余方程,x的指数为1,所以叫线性同余方程
转化成不定方程即可,可以化为ax + my = b
如果有解,恰有d个解
特殊地,如果b=1,即ax = 1(mod m) x为amodm的逆
这时可推出gcd(a, m) = 1即a与m互质才有解
「NOIP2012」同余方程(线性同余方程)
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
void exgcd(ll a, ll b, ll& d, ll& x, ll& y)
{
if(!b) { d = a; x = 1; y = 0; }
else { exgcd(b, a % b, d, y, x); y -= x * (a / b); }
}
int main()
{
ll a, b, x, y, d;
scanf("%lld%lld", &a, &b);
exgcd(a, b, d, x, y);
ll mod = b / d;
x %= mod; if(x < 0) x += mod;
printf("%lld\n", x);
return 0;
}
2.14(同余)
乘法逆元
ax=1(mod p) x为逆元
逆元存在前提是a与p互质
当p为质数时可以用费马小定理,a^p-2即为逆元,此时互质等价于a不是p的倍数
当p不为质数时解同余方程,用扩展欧几里德
「一本通 6.4 例 3」Sumdiv(等比数列求和+逆元)
用约数和公式,加等比数列求和
涉及到除法,也就是要用逆元
9901是质数,所以只要p-1不是9901的倍数就可以用费马小定理
这里特判一下,如果是p-1是9901的倍数,那么p mod 9901 = 1 1+p+p*p……p^n = n + 1
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MOD = 9901;
vector<int> ve;
int cnt[30], m;
ll add(ll a, ll b) { return (a + b) % MOD; }
ll mul(ll a, ll b) { return a * b % MOD; }
ll binpow(ll a, ll b)
{
ll res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = mul(res, a);
a = mul(a, a);
}
return res;
}
ll inv(ll a) { return binpow(a, MOD - 2); }
int main()
{
ll a, b;
scanf("%lld%lld", &a, &b);
for(int i = 2; i * i <= a; i++)
if(a % i == 0)
{
ve.push_back(i);
while(a % i == 0)
{
a /= i;
cnt[m]++;
}
m++;
}
if(a > 1) ve.push_back(a), cnt[m++] = 1;
ll ans = 1;
REP(i, 0, ve.size())
{
ll p = ve[i], n = cnt[i] * b;
if((p - 1) % MOD == 0)
{
ans = mul(ans, n + 1);
continue;
}
ll t = binpow(p, n + 1);
t--; if(t < 0) t += MOD;
ans = mul(ans, mul(t, inv(p - 1)));
}
printf("%lld\n", ans);
return 0;
}
中国剩余定理
其实蛮简单的,可以用来解同余方程组
x = a1(mod m1)
x = a2(mod m2)
.
.
x = an(mod mn)
其中m1, m2……mn两两互质
设M = m1 * m2……*mn
Mi = M / mi
设ti为Mi模mi的逆元(mi不一定为质数,所以要用扩展欧几里德)
那么答案x = a1M1t1 + a2M2t2……
很容易正面,把x代入上面任意一个方程都成立
这个思路有点像拉格朗日插值法
这个解像是凑出来的
这个解在modM意义下有唯一解
「一本通 6.4 例 4」曹冲养猪(中国剩余定理模板)
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXN = 1e3 + 10;
ll a[MAXN], m[MAXN], M = 1;
void exgcd(ll a, ll b, ll& d, ll& x, ll& y)
{
if(!b) { d = a; x = 1; y = 0; }
else { exgcd(b, a % b, d, y, x); y -= x * (a / b); }
}
int main()
{
int n; scanf("%d", &n);
_for(i, 1, n)
{
scanf("%lld%lld", &m[i], &a[i]);
M *= m[i];
}
ll ans = 0;
_for(i, 1, n)
{
ll Mi = M / m[i];
ll x, y, d;
exgcd(Mi, m[i], d, x, y);
ans += a[i] * Mi * x;
}
printf("%lld\n", (ans % M + M) % M);
return 0;
}
扩展欧几里德解同余方程组
理解了就简单了
就是利用前面的解
假设前k-1个同余方程的解为x
mi的lcm为M
那么通解就为x+kM
那么把这个代到第k个方程组
x+kM=ak(mod mk)
exgcd解方程组就OK了
如果无解那么这个同余方程组就无解
「一本通 6.4 例 5」Strange Way to Express Integers(扩展欧几里德解同余方程组模板)
有个细节,不然wa一个点
就是过程中要始终控制数据的大小,不然会爆long long
两个地方
一个是每次x算完要mod M
另一个是欧几里德算出解后要取模控制一下大小,不要忘了这一步
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
ll a[MAXN], m[MAXN];
ll gcd(ll a, ll b) { return !b ? a : gcd(b, a % b); }
ll lcm(ll a, ll b) { return a / gcd(a, b) * b; }
void exgcd(ll a, ll b, ll& d, ll& x, ll& y)
{
if(!b) { d = a; x = 1; y = 0; }
else { exgcd(b, a % b, d, y, x); y -= x * (a / b); }
}
int main()
{
int n;
while(~scanf("%d", &n))
{
_for(i, 1, n) scanf("%lld%lld", &m[i], &a[i]);
ll x = a[1], M = m[1], flag = 1;
_for(i, 2, n)
{
ll x0, y0, d;
exgcd(M, m[i], d, x0, y0);
if((a[i] - x) % d != 0)
{
flag = 0;
break;
}
x0 *= (a[i] - x) / d % (m[i] / d);
x += x0 * M;
M = lcm(M, m[i]);
x %= M;
}
if(!flag) puts("-1");
else printf("%lld\n", (x % M + M) % M);
}
return 0;
}
2.15(同余)
「一本通 6.4 练习 1」荒岛野人(枚举答案验证+exgcd)
这道题想挺久没什么思路,看了题解
我卡在Li这个点不知道这么处理
我开始以为要枚举时间,然后每一次只留下活着的去判断
但发现不知道这么判断
因为这个不相等实在是太不好搞了,完全没思路想着这么弄出个M出来
没想到题解是直接枚举答案M
这是一个很经典的思路
不知道怎么得出答案,那就枚举答案来验证。尤其是在这种求最大最小的题目中
其实我一开始有想到二分答案,但发现它不满足二分答案的性质
没想到已经很靠近了,就是枚举答案,最终复杂度也不高
枚举M,然后枚举两个人是否有可能相等,列一个方程
方程无解或者相等的天数有人死了就是ok的
另外用exgcd时有两个我要注意的地方
一个是类型是void,我老是写成ll
一个是a和b一定要都为正数,exgcd前要判断一下
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
ll c[20], p[20], l[20], m;
int n;
void exgcd(ll a, ll b, ll& d, ll& x, ll& y)
{
if(!b) { d = a; x = 1; y = 0; }
else { exgcd(b, a % b, d, y, x); y -= x * (a / b); }
}
bool check(int i, int j)
{
ll d, x, y;
if(p[i] - p[j] < 0) swap(i, j);
exgcd(p[i] - p[j], m, d, x, y);
if((c[j] - c[i]) % d != 0) return true;
x *= (c[j] - c[i]) / d;
ll mod = m / d;
return (x % mod + mod) % mod > min(l[i], l[j]);
}
bool work()
{
_for(i, 1, n)
_for(j, i + 1, n)
if(!check(i, j))
return false;
return true;
}
int main()
{
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%lld%lld%lld", &c[i], &p[i], &l[i]);
m = max(m, c[i]);
}
while(!work()) m++;
printf("%lld\n", m);
return 0;
}
还有一个高次同余方程的知识点,用BGBS算法,不太想弄,以后遇到了再说吧
今天没什么兴致训练了
明天开启组合数学吧
2.16(组合数学)
首先复习了n方组合数递推,用来预处理组合数
n方预处理组合数
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
int c[MAXN][MAXN]; //c[i][j]表示从i个数中选j个
void init()
{
REP(i, 0, MAXN) c[i][0] = 1; //初始化,要不递推个啥,全是0。这个初始化包含所有0的情况,下面递推ij就不用到0了
REP(i, 1, MAXN)
_for(j, 1, i)
c[i][j] = c[i-1][j] + c[i-1][j-1]; //把一个数提出来,i-1,这个数选不选就是j和j-1
}
int main()
{
init();
_for(i, 1, 10)
_for(j, 1, i)
printf("i: %d j: %d c: %d\n", i, j, c[i][j]);
return 0;
}
「NOIP2011」计算系数(二项式定理)
高中数学知识
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
const int MOD = 10007;
int c[MAXN][MAXN];
int add(int a, int b) { return (a + b) % MOD; }
int mul(int a, int b) { return 1ll * a * b % MOD; }
void init()
{
REP(i, 0, MAXN) c[i][0] = 1;
REP(i, 1, MAXN)
_for(j, 1, i)
c[i][j] = add(c[i-1][j], c[i-1][j-1]);
}
int cal(int a, int b)
{
int res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = mul(res, a);
a = mul(a, a);
}
return res;
}
int main()
{
init();
int a, b, k, n, m;
scanf("%d%d%d%d%d", &a, &b, &k, &n, &m);
printf("%d\n", mul(c[k][m], mul(cal(a, n), cal(b, m))));
return 0;
}
不知道为什么最近有点累了……
我21号回学校,发现我的寒假只有五六天了
寒假大部分时间都在编程训练,最后几天给自己缓缓吧
想干嘛干嘛吧,编程的话看心情
2.21
今天敲一敲模板吧,今晚回校明天就开始集训了
我发现这几天少了编程,生活真的空虚了很多
就是很无聊,也不知道干啥。
像出去玩,看书看电影啥的,可以当做生活的调剂,但你要我一整天都干这个那就太无聊了
还是编程好
生活大部分是编程,小部分时间可以娱乐调剂一下
其实ACM本来就是一件挺快乐的事情
到时候开学时候也是用它来填补我的课余生活
如果没有ACM的话我的课余生活挺空虚的
能长期坚持一件事情,一定靠的是热爱
ACM,读书都是这样
真的喜欢它