1.前缀和算法
eg1:P8160 [JOI 2022 Final] 星际蛋糕
思想:暴力枚举------>前缀和+二分优化
#include <algorithm>
#include <iostream>
using namespace std;
int ans;
const int N = 200005;
long long n, q,a[N], num[N], s[N], x;//不开long long见祖宗!!!!
int main()
{
scanf("%lld", &n);
for (int i = 1; i <= n; i++)
{
scanf("%lld", &a[i]);
num[i] = 1;
while (!(a[i] & 1))//a[i] & 1 的含义是判断 a[i] 是否为奇数,因为 1 的二进制表示是
//0001,而奇数的二进制表示的最低位一定是 1。
{
a[i] >>= 1;
num[i] <<= 1;
}
s[i] = s[i - 1] + num[i];//求数量前缀和
}
scanf("%lld", &q);
while (q--)
{
scanf("%lld", &x);
int ans = a[(lower_bound(s + 1, s + 1 + n, x) - s)];//lower_bound,C++库里内置的二分函数,返回区间中第一个大于等于 x 的数的地址,减去s就可以得到下标。
printf("%d\n", ans);
}
}
tips:
-
前缀和,差分,循环下标都从1开始
-
整体被切分成若干个体,并计算某个区间的和----->前缀和思想
-
C++STL二分查找lower_bound
2.双指针算法
eg1:P7714 「EZEC-10」排列排序
思想:双指针维护单个序列(快慢指针)
小技巧:因为此题为从1---n的升序序列,所以数组的下标对应该元素的值。(数字1的下标为1,数字2的下标为2,依此类推)
#include <algorithm>
#include <iostream>
using namespace std;
int n, T, a[1000005], maxx, ans;
int main()
{
scanf("%d", &T);
while (T--)
{
ans = 0;//每一次操作前进行初始化
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int i = 1;
while (i <= n)
{
if (a[i] == i)//数组的下标与该元素的值相同,i指针向后移动一位
i++;
else
{
int maxx = a[i];//maxx为j指针的移动的最大范围
int j = i + 1;
maxx = max(maxx, a[j]);//j指针移动的最远位置不能超过i--j之间的元素最大值
while (maxx > j)
{
j++;
maxx = max(maxx, a[j]);
}
ans += j - i + 1;
i = j + 1;
}
}
printf("%d\n", ans);
}
return 0;
}
3.区间合并
eg1:P1496 火烧赤壁
思想1:区间合并
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef pair<long long, long long> pll;//题目数据范围较大,开long long
int n;
vector<pll> segs;
long long ans;
void merge(vector<pll> &segs)
{
vector<pll> res;
sort(segs.begin(), segs.end());//将区间的起点排序
long long st = -2e31, ed = -2e31;//初始化:起点和终点都为数据范围最小值-2e31
for (auto seg : segs)
if (ed < seg.first)//若前一段区间的终点小于下一段区间的起点,则增加一个新的区间
{
if (st != -2e31)
res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else
ed = max(ed, seg.second);//否则更新区间的终点
if (st != -2e31)
res.push_back({st, ed});//防止输入的空间segs为空
segs = res;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
long long l, r;
scanf("%lld%lld", &l, &r);
segs.push_back({l, r});
}
merge(segs);
for (auto seg : segs)
ans += seg.second - seg.first;
printf("%lld", ans);
return 0;
}
思想2:离散化+差分
思路 :
- 题目数据范围大,个数少----->离散化
- 问题转化为在一个数列上,每次给 [l,r] 区间内的数都增加 1。 最后求出数列的末状态,看有多少个点不是 0。但是我们直接模拟的话时间复杂度会过高,所以我们可以在离散化后进行差分,最后进行求前缀和。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef pair<int, int> pii;
int n, ans = 0;
const int N = 4e4 + 5;//每组数据有两个,所以开两倍才能存完
int b[N], s[N];
vector<int> alls;//存储所有待离散化的值
vector<pii> add, query;
int find(int x)//二分求出x对应离散化的值
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = (l + r) / 2;
if (alls[mid] >= x)
r = mid;
else
l = mid + 1;
}
return r + 1;//+1时方便做差分求和(前缀和从1开始比较方便,防止下标是-1的越界)
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
int l, r;
scanf("%d%d", &l, &r);
query.push_back({ l, r });
alls.push_back(l);
alls.push_back(r);
}
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
for (auto item : query)
{
int l = find(item.first), r = find(item.second);
b[l]++, b[r]--;//这里+1和二分+1二选一
} //题目说明不包含右端点,因此为b[r]--,不是b[r+1]--
for (int i = 1; i <= alls.size(); i++)
s[i] += s[i - 1] + b[i];//差分并求前缀和
int l, r;//l表示起始位置下标,r表示终止位置下标
//以样例为例
// -1 1 2 5 9 11 alls[]
// 1 2 3 4 5 6 离散后的数组下标
// 0 1 0 1 2 1 0 s[]
//第一段火是[-1,1),s[i]>0,s[i-1]==0为起始位置,s[i]==0,s[i-1]>0为终止位置
for (int i = 1; i <= alls.size(); i++)
{
if (s[i] != 0 && s[i - 1] == 0)
l = i - 1;//l是差分数组的下标,实际下标要-1
//if (s[i] != 0 && s[i + 1] == 0)
//{
// r = i;//i+1的数才是终点,再-1得i
// ans += alls[r] - alls[l];//通过下标取出alls数组中实际的值
//}
if (s[i] == 0 && s[i - 1] != 0)
{
r = i - 1;//i是终点,再-1得i-1
ans += alls[r] - alls[l];//通过下标取出alls数组(离散化前的数组)中实际的值
}//两个if一个意思
}
printf("%d", ans);
return 0;
}
4.链表
eg1:4654. 消除游戏
思想:双链表快速完成删除操作
#include <algorithm>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6 + 10;
char s[N];
vector<int> w, q;//w为待删数组,q为备份数组
int l[N], r[N];
bool st[N];
void insert(int k)
{
if (!st[k])//特判:防止某个数据被多次删除 eg:$$@@
{
st[k] = true;
w.push_back(k);
}
}
void fun()
{
w.clear();
for (int k : q)
{
int a = l[k], b = k, c = r[k];
if (s[a] == s[b] && s[b] != s[c] && s[c] != '#')
{
insert(b);
insert(c);
}
else if (s[a] != s[b] && s[b] == s[c] && s[a] != '#')
{
insert(a);
insert(b);
}
}
}
int main()
{
scanf("%s", s + 1);
int n = strlen(s + 1);
s[0] = s[n + 1] = '#';//规定链表两端边界,防止越界问题
for (int i = 1; i <= n; i++)
{
l[i] = i - 1, r[i] = i + 1;
q.push_back(i);
}//构建链表数据结构
r[0] = 1, l[n + 1] = n;//初始化链表
while (true)
{
fun();
if (w.empty())//判空
break;
q.clear();
for (int k : w)
{
int a = l[k], b = k, c = r[k];
if (!st[a] && a && (q.empty() || a != q.back()))
q.push_back(a);
if (!st[c] && c != n + 1)
q.push_back(c);
r[a] = c, l[c] = a;//双链表的删除操作
}
}
if (r[0] == n + 1)
puts("EMPTY");
else
{
for (int i = r[0]; i != n + 1; i = r[i])
printf("%c", s[i]);
}
return 0;
}
eg2:136. 邻值查找
思想:双链表快速完成删除操作
思路:
- 寻找与A[i]差值最小的数------>排序操作(排序后A[i]左右两个数即为差值最小的数)
- 先从第n个数开始枚举,然后逆序输出----->因为题目中说寻找A[i]之前与A[i]差值最小的数,所有数都在n前面,不用担心顺序问题
- 第n个数枚举完后,删除第n个数,继续向前遍历
#include <algorithm>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
typedef pair<long long, int> pli;
int n;
int p[N], l[N], r[N];//p[n]存储排序后该数在链表中所对应的下标
pli a[N], ans[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i].first;
a[i].second = i;
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++)
{
l[i] = i - 1, r[i] = i + 1;//初始化链表
p[a[i].second] = i;
}
a[0].first = -4e9, a[n + 1].first = 4e9;//设置哨兵防止越界
for (int i = n; i > 1; i--)
{
int j = p[i], left = l[j], right = r[j];
long long lv = abs(a[left].first - a[j].first);
long long rv = abs(a[right].first - a[j].first);
if (lv <= rv)
ans[i] = {lv, a[left].second};
else
ans[i] = {rv, a[right].second};
l[right] = left, r[left] = right;//删除第n个节点
}
for (int i = 2; i <= n; i++)
cout << ans[i].first << ' ' << ans[i].second << endl;
return 0;
}
5.栈
eg1:150. 括号画家
思想:利用栈完成括号匹配
小技巧:括号序列常利用栈的数据结构
#include <algorithm>
#include <cstring>
#include <iostream>
#include <stack>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int res = 0;
string str;
cin >> str;
stack<int> stk;//利用栈存储括号序列的下标,方便计算美观括号序列的长度
for (int i = 0; i < str.size(); i++)
{
char c = str[i];
if (stk.size())
{
char t = str[stk.top()];
if (c == ')' && t == '(' || c == ']' && t == '[' || c == '}' && t == '{')
stk.pop();
else
stk.push(i);
}
else
stk.push(i);
if (stk.size())
res = max(res, i - stk.top());
else
res = max(res, i + 1);
}
cout << res << endl;
return 0;
}
eg2:1883. 删减 (字符串中删除指定子串)
思想:栈----->只用在结尾进行增删操作
#include <algorithm>
#include <cstring>
#include <iostream>
#include <stack>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s, t;
cin >> s >> t;
string stk;//利用STL中的string模拟栈
for(auto c:s)
{
stk += c;
while(stk.size()>=t.size()&&stk.substr(stk.size()-t.size())==t)
stk.erase(stk.end() - t.size(), stk.end());
}
cout << stk << endl;
return 0;
}
eg3:131. 直方图中最大的矩形
思想:单调栈------>找出每个数左(右)边离它最近的比它大(小)的数
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
int n;
int h[N], l[N], r[N], stk[N];//栈存的是数组的下标
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (scanf("%d", &n), n)//输入包含几个测试用例,当输入用例为n=0时,结束输入
{
for (int i = 1; i <= n; i++)
scanf("%d", &h[i]);
h[0] = h[n + 1] = -1;//设置哨兵防止数组越界
int tt = 0;
stk[0] = 0;
for (int i = 1; i <= n; i++)//找出每个数左边离它最近的比它小的数
{
while (tt && h[i] <= h[stk[tt]])
tt--;//删除栈顶
l[i] = stk[tt];
stk[++tt] = i;
}
tt = 0;//将栈更新成只有一个元素
stk[0] = n + 1;
for (int i = n; i > 0; i--)//找出每个数右边离它最近的比它小的数
{
while (tt && h[i] <= h[stk[tt]])
tt--;//删除栈顶
r[i] = stk[tt];
stk[++tt] = i;
}
ll res = 0;
for (int i = 1; i <= n; i++)
res = max(res, (ll)h[i] * (r[i] - l[i] - 1));
printf("%lld\n", res);
}
return 0;
}
eg4:152. 城市游戏
思想:模拟/递推+单调栈
思路:本题为eg3的加强版,可将模型抽象成eg3(见下图)
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int n, m;
int s[N][N], l[N], r[N], stk[N];
int work(int h[])//h表示每个矩形高度(这个函数就是eg3的原代码)
{
h[0] = h[m + 1] = -1;
int tt = 0;
stk[0] = 0;
for (int i = 1; i <= m; i++)
{
while (tt && h[i] <= h[stk[tt]])
tt--;
l[i] = stk[tt];
stk[++tt] = i;
}
tt = 0;
stk[0] = m + 1;
for (int i = m; i > 0; i--)
{
while (tt && h[i] <= h[stk[tt]])
tt--;
r[i] = stk[tt];
stk[++tt] = i;
}
int res = 0;
for (int i = 1; i <= m; i++)
res = max(res, h[i] * (r[i] - l[i] - 1));
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
char c;
cin >> c;
if (c == 'F')
s[i][j] = s[i - 1][j] + 1;//递推求出累计F及其上方一共有几个F
}
int res = 0;
for (int i = 1; i <= n; i++)
res = max(res, work(s[i]));
cout << 3 * res << endl;
return 0;
}
6.队列
eg1:496. 机器翻译
思想:队列------>先进先出
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e3 + 10;
int n, m;
int res = 0;
bool st[N];//判断是否在队列中
queue<int> q;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
while (n--)
{
int x;
cin >> x;
if (!st[x])
{
if (q.size() == m)
{
st[q.front()] = false;
q.pop();
}
q.push(x);
st[x] = true;
res++;
}
}
cout << res << endl;
return 0;
}
eg2:154. 滑动窗口
思想:单调队列
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int a[N], q[N];//队列中存的是数组下标
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
cin >> a[i];
int hh = 0, tt = -1;//初始化队列
for (int i = 0; i < n; i++)
{
while (hh <= tt && q[hh] < i - m + 1)//判断队头是否划出窗口
hh++;//弹出队头
while (hh <= tt && a[q[tt]] >= a[i])
tt--;//弹出队尾
q[++tt] = i;//向队尾插入一个数
if (i >= m - 1)
cout << a[q[hh]]<<" ";
}
puts("");
hh = 0, tt = -1;
for (int i = 0; i < n;i++)
{
while (hh <= tt && q[hh] < i - m + 1)//判断队头是否划出窗口
hh++;//弹出队头
while (hh <= tt && a[q[tt]] <= a[i])
tt--;//弹出队尾
q[++tt] = i;//向队尾插入一个数
if (i >= m - 1)
cout << a[q[hh]]<<" ";
}
return 0;
}
eg3:1162. 公交换乘
思想:滑动窗口+队列模拟
思路:
我们可以从前往后扫描每条记录,同时用一个队列维护当前车次可以使用的优惠券区间(区间为45分钟):
- 如果当前记录是火车,则加入维护的优惠券区间;
- 如果当前记录是公交车,则线性扫描一遍队列中所有优惠券,找到第一个未被使用过的且大于等于当前价格的优惠券即可;
- 可以用一个bool数组对优惠券判重,以保证每张优惠券最多只被用一次。
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int res = 0;
struct ticket
{
int time, price;
bool used;
} q[N];
int main()
{
scanf("%d", &n);
int l = 0, r = 0;//队列初始化
for (int i = 0; i < n; i++)
{
int type, price, time;
scanf("%d%d%d", &type, &price, &time);
if (type == 0)
{
res += price;
q[r++] = {time, price};
}
else
{
while (l <= r && time - q[l].time > 45)//维护一个滑动窗口
l++;
bool success = false;
for (int j = l; j <= r; j++)
{
if (q[j].used == false && q[j].price >= price)
{
q[j].used = true;
success = true;
break;
}
}
if (!success)
res += price;
}
}
printf("%d\n", res);
return 0;
}
eg4:4964. 子矩阵
思想:二维滑动窗口
思路:二维的单调队列,等价于a个一维的情况中的最小值
第一步:对于每一行,求长度为b的窗口的最大值/最小值
第二步:在第一步的基础上,对于每一列,求长度(可以理解为高度)为a的窗口的最大值/最小值
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e3 + 10, mod = 998244353;
typedef long long ll;
int n, m,a,b;
int w[N][N], rmax[N][N], rmin[N][N], q[N];
int res = 0;
void get_max(int a[], int b[], int tot, int k)//滑动窗口求最大值
{
int hh = 0, tt = -1;
for (int i = 0; i < tot; i++)
{
while (hh <= tt && q[hh] <= i - k)
hh++;
while (hh <= tt && a[q[tt]] <= a[i])
tt--;
q[++tt] = i;
b[i] = a[q[hh]];//取出队首元素
}
}
void get_min(int a[], int b[], int tot, int k)//滑动窗口求最小值
{
int hh = 0, tt = -1;
for (int i = 0; i < tot; i++)
{
while (hh <= tt && q[hh] <= i - k)
hh++;
while (hh <= tt && a[q[tt]] >= a[i])
tt--;
q[++tt] = i;
b[i] = a[q[hh]];//取出队首元素
}
}
int main()
{
cin >> n >> m >> a >> b;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> w[i][j];
for (int i = 0; i <n; i++)//对于每一行,求长度为b的窗口的最大值/最小值
{
get_max(w[i], rmax[i], m, b);
get_min(w[i], rmin[i], m, b);
}
int A[N], B[N], C[N];
for (int i = b - 1; i < m; i++)//在第一步的基础上,对于每一列,求长度(可以理解为高度)为a
//的窗口的最大值/最小值
{
for (int j = 0; j < n; j++)
A[j] = rmax[j][i];
get_max(A, B, n, a);
for (int j = 0; j < n; j++)
A[j] = rmin[j][i];
get_min(A, C, n, a);
for (int j = a - 1; j < n; j++)
res = (res + (ll)B[j] * C[j]) % mod;//转换成long long,防止爆int
}
cout << res << endl;
return 0;
}
7.KMP
eg1:141. 周期
思想:求next数组------>最小循环节t=n-next[n]
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
char p[N];
int ne[N];//next数组:最大的后缀与前缀相等
int main()
{
int T = 1;
while (scanf("%d", &n), n)
{
scanf("%s", p + 1);
for (int i = 2, j = 0; i <= n; i++)//KMP算法求next数组
{
while (j && p[i] != p[j + 1])
j = ne[j];
if (p[i] == p[j + 1])
j++;
ne[i] = j;
}
printf("Test case #%d\n", T++);
for (int i = 1; i <= n; i++)
{
int t = i - ne[i];//求循环节t
if (i % t == 0 && i / t > 1)
printf("%d %d\n", i, i / t);
}
puts("");
}
return 0;
}
eg2:3823. 寻找字符串
思想:用next数组求字符串中所有的前缀=后缀
思路:
- 如果说l1=next[i]是最长的公共前后缀,那么次长的公共前后缀是l2=next[l1],再次长的公共前后缀是l3=next[l2].........依此类推可以求字符串中所有的公共前后缀
- 把所有的next[i]都存到一个bool数组里面去,然后找到第一个在前面出现过的数,就是我们的答案
------>如果这个长度的字符串也被标记过,那么就说明满足要求,
如果不合法的话,就再找当前最长公共前后缀的最长公共前后缀,那么这个最长公共前后缀就 会在前面出现过,然后也会在中间出现过
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
char p[N];
int ne[N];
bool st[N];
int main()
{
int T;
scanf("%d",&T);
while (T--)
{
scanf("%s", p + 1);
n = strlen(p + 1);
for (int i = 2, j = 0; i <= n; i++)//求next[]数组
{
while (j && p[i] != p[j + 1])
j = ne[j];
if (p[i] == p[j + 1])
j++;
ne[i] = j;
}
for (int i = 0; i <= n; i++)//先初始化
st[i] = false;
for (int i = 1; i<n; i++)//标记的是2~n-1所有出现过的数
st[ne[i]] = true;
int res = 0;
for (int i = ne[n]; i; i = ne[i])
if (st[i])
{
res = i;
break;
}
if(!res)
puts("not exist");
else
{
p[res + 1] = 0;//输出技巧,直接打上结尾标记,后面的就直接不输出了
printf("%s\n", p + 1);
}
}
return 0;
}
8.Trie树(字典树、前缀树)
一般来说,用到 Trie的题目中的字母要么全是小写字母,要么全是大写字母,要么全是数字,要么就是0和1,也就是说字符的个数不是很多。
------>主要处理前缀问题
insert(插入)
当需要插入一个字符串 s 时,我们令一个指针 p起初指向根节点。
然后,依次扫描 s中的每个字符 c,当 s中的字符扫描完毕时,在当前指针 p上标记它是一个字符串的末尾。
eg1:142. 前缀统计
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int son[N][26];//存储树中每个节点的子节点
int cnt[N];//存储以每个节点结尾的单词数量
char str[N];
int idx;//指向当前节点
void insert(char *str)//插入字符串
{
int p = 0;//初始化根节点
for (int i = 0; str[i]; i++)
{
int u = str[i] - 'a';
if (!son[p][u])
son[p][u] = ++idx;//若无此节点,添加此节点
p = son[p][u];//向前遍历
}
cnt[p]++;//标记当前节点存在单词
}
int query(char *str)//查询字符串出现的次数
{
int p = 0,res=0;
for (int i = 0; str[i]; i++)
{
int u = str[i] - 'a';
if (!son[p][u])
return res;
p = son[p][u];
res += cnt[p];
}
return res;
}
int main()
{
scanf("%d%d", &n, &m);
while (n--)
{
scanf("%s", str);
insert(str);
}
while (m--)
{
scanf("%s", str);
printf("%d\n", query(str));
}
return 0;
}
eg2:161. 电话列表
思路:
- 每次先把号码一个一个存进str数组,每个数字位都++
- 判断如果一个号码每一位出现次数均大于1,说明其为前缀,输出”NO”
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
int n, m;
int son[N][10];
int cnt[N];
char str[N][10];
int idx;
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i++)
{
int u = str[i] - '0';
if (!son[p][u])
son[p][u] = ++idx;
p = son[p][u];
cnt[p]++;//把号码一个一个存进str数组,每个数字位都++
}
}
bool check(char *str)
{
int p = 0;
for (int i = 0; str[i]; i++)
{
int u = str[i] - '0';
if (cnt[son[p][u]] == 1)//如果一个号码为1,那么这个号码之后的所有号码次数都为1
return true;
p = son[p][u];
}
return false;
}
int main()
{
scanf("%d", &n);
while (n--)
{
int flag = 0;
idx=0;//每次操作前进行初始化
memset(son[0], 0, sizeof son);
memset(cnt, 0, sizeof cnt);
scanf("%d", &m);
for (int i = 0; i < m; i++)
{
scanf("%s", str[i]);
insert(str[i]);
}
for (int i = 0; i < m; i++)
{
if (!check(str[i]))
{
flag = 1;
break;
}
}
if (flag == 1)
puts("NO");
else
puts("YES");
}
return 0;
}
eg3:4191. 加密信息
思路:此题相当于eg1和eg2的综合版(N串存入Trie数中,M串为被查询串)
- 当M串比N串长度大 ,运用eg1的思路,可统计所有前缀数量
- 当M串比N串长度小,则运用eg2的思路
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 500010;
int n, m;
int son[N][2];
int cnt[N], st[N];
char str[N][10];
int idx;
void insert(string str)
{
int p = 0;
for (int i = 0; str[i]; i++)
{
int u = str[i] - '0';
if (!son[p][u])
son[p][u] = ++idx;
p = son[p][u];
st[p]++;//这里是记录经过p结点的字符串数量,因为这里可能会重复经过某一个结点就例如
//样例中的第二条解密信息,他经过了3个相同的1,这里就是为了记录那种情况
}
cnt[p]++;//这里就是记录以p为结点的字符串的数量
}
int query(string str)
{
int p = 0, res = 0;
for (int i = 0; str[i]; i++)
{
int u = str[i] - 'a';
if (!son[p][u])//处理的是M串比N串长度大的情况,如果这个点接下来不再匹配
//返回的就是以这个点为尾结点的字符串数量
return res;
p = son[p][u];
res += cnt[p];
}
return res + st[p] - cnt[p];//这里就是处理M串比N串中字符串长度小的情况
//st[p]-ed[p]就是p结点之后的字符串的个数
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
{
int k;
string s;
cin >> k;
for (int j = 1; j <= k; j++)//因为空格也是字符,运用字符串拼接防止空格被读入字
//符串
{
char c;
cin >> c;
s += c;
}
insert(s);
}
for (int i = 1; i <= m; i++)
{
int k;
string s;
cin >> k;
for (int j = 1; j <= k; j++)
{
char c;
cin >> c;
s += c;
}
printf("%d\n", query(s));
}
return 0;
}
9.并查集
eg1:3719. 畅通工程
思路:用并查集来维护多个集合,若两个元素不在同一个集合当中,则将这两个元素添加到一个集合当中形成联通块,联通块的数量-1即为答案
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 10010;
int n, m;
int p[N];
int find(int x)
{
if(p[x]!=x)
p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d", &n,&m);
for (int i = 1; i <= n;i++)
p[i] = i;
int cnt = n;//初始化:每个元素自己与自己连接(类似于n个联通块)
for (int i = 1; i <= m;i++)
{
int a, b;
scanf("%d%d", &a, &b);
if(find(a)!=find(b))
{
p[find(a)] = find(b);
cnt--;
}
}
printf("%d", cnt - 1);
return 0;
}
10.堆(二叉树结构)
eg1:4072. 习题册
思想:小根堆
思路:
- 实现删除任意一个元素------->堆
- 学生每次挑选最便宜的练习册---------->小根堆
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int N = 2e5 + 10;
typedef pair<int, int> pii;
int n, m;
int p[N];
priority_queue<pii, vector<pii>, greater<pii>> h[3];//开三个小根堆存三种知识点的书
bool st[N];//判断该书是否已经买过
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &p[i]);
for (int j = 0; j < 2; j++)
for (int i = 0; i < n; i++)
{
int a;
scanf("%d", &a);
h[a - 1].push({p[i], i});
}
scanf("%d", &m);
while (m--)
{
int c;
scanf("%d", &c);
c--;
while (h[c].size() && st[h[c].top().second])
h[c].pop();
if (h[c].empty())
printf("-1 ");
else
{
auto t = h[c].top();
h[c].pop();
printf("%d ", t.first);
st[t.second] = true;
}
}
return 0;
}
eg2:54. 数据流中的中位数
思想:处理动态中位数-------->对顶堆
思路:
如果两个堆的大小相差不超过1,较大的那个堆的堆顶必定是中位数(偶数个数时中位数是排序后中间的两个之一)
class Solution
{
public:
priority_queue<int, vector<int>, greater<int>> min_heap;
//小根堆:维护集合中较大值的部分的最小值
priority_queue<int> max_heap;//大根堆:维护集合中较小值的部分的最大值
void insert(int num)
{
max_heap.push(num);//将所有插入的数都先加到大根堆,然后在进行交换
if (min_heap.size() && max_heap.top() > min_heap.top())
{//如果大根堆的堆头>小根堆的堆头,则交换两个数
auto maxv = max_heap.top(), minv = min_heap.top();
max_heap.pop(), min_heap.pop();
max_heap.push(minv), min_heap.push(maxv);
}
if (max_heap.size() > min_heap.size() + 1)
{//如果两个堆的大小相差超过1,将大根堆的堆头转移到小根堆的堆头
min_heap.push(max_heap.top());
max_heap.pop();
}
}
double getMedian()
{
if (max_heap.size() + min_heap.size()&1)
return max_heap.top();
return (max_heap.top() + min_heap.top()) / 2.0;
}
};
eg3:3492. 负载均衡
思想:小根堆
理解:设第i次任务开始时间为a[i],只有结束时间在a[i]之前才会影响算力是否够用
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 200010;
int n,m;
int s[N];//每台计算机算力
priority_queue<PII,vector<PII>,greater<PII> >q[N];//每台计算机任务
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++ i)scanf("%d",&s[i]);//!下标从1开始
while (m -- ){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
while(q[b].size()&&q[b].top().x<=a){//当堆尾的结束时刻小于当前分配时刻
s[b] += q[b].top().y;//!恢复算力
q[b].pop();//弹出该任务
}
if(s[b]<d)puts("-1");//当前算力无法分配
else{
q[b].push({a+c,d});//加入任务
s[b] -= d;//更新算力
printf("%d\n",s[b]);//输出算力
}
}
return 0;
}
11.哈希表
eg1:3779. 相等的和
思想:快速完成增删改查操作------->哈希表
思路:
- 先求出各个序列的和,再枚举去掉各个数之后和可能出现的情况,存入哈希表
- 若两个不同序列的和相同,则输出答案
#include <algorithm>
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 2e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
int w[N];
int main()
{
int n;
scanf("%d", &n);
unordered_map<int, pii> s;
for (int i = 1; i <= n; i++)
{
int m, sum = 0;
scanf("%d", &m);
for (int j = 1; j <= m; j++)
{
scanf("%d", &w[j]);
sum += w[j];
}
for (int j = 1; j <= m; j++)
{
int t = sum - w[j];//当前序列能够得到的和有哪些
if (s.count(t) && s[t].first != i)//哈希表中存在该和t,并且行号j和i不相同,说明找
//到了不在同一行的数
{
puts("YES");
printf("%d %d\n", s[t].first, s[t].second);
printf("%d %d\n", i, j);
return 0;
}
s[t] = {i, j};//加入元素
}
}
puts("NO");
return 0;
}
12.DFS
eg1:5147. 数量
思路:暴力枚举(TLE)------->DFS
思路:搜索该数的每一个数位,只有4和7两种选择
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;
int n;
int ans;
void dfs(ll u)//开long long防止爆int
{
if (u>n)
return;
if(u)
ans++;
dfs(u * 10 + 4);
dfs(u * 10 + 7);
}
int main()
{
scanf("%d", &n);
dfs(0);//从第0层开始搜索
printf("%d", ans);
return 0;
}
eg2:2005. 马蹄铁
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 5;
int n;
int ans;
char g[N][N];//读入坐标数据
bool st[N][N];//判断该位置是否被走过
void dfs(int x, int y, int l, int r)
{
st[x][y] = true;
if (l == r)
{
ans = max(ans, l + r);
st[x][y] = false;//回溯(还原现场)
return;
}
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//表示坐标偏移量:上下左右
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < n && b >= 0 && b < n && !st[a][b])
{
if (g[x][y] == ')' && g[a][b] == '(')//特判
continue;
if (g[a][b] == '(')
dfs(a, b, l + 1, r);
else
dfs(a, b, l, r + 1);
}
}
st[x][y] = false;//回溯(还原现场)
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%s", g[i]);
if (g[0][0] == '(')
dfs(0, 0, 1, 0);
printf("%d", ans);
return 0;
}
eg3:1613. 数独简单版
法一:剪枝(时间复杂度低)
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 10;
typedef long long ll;
char g[N][N];
bool row[N][N], col[N][N], cell[3][3][N];
bool dfs(int x, int y)
{
if (y == 9)//搜到最后一列,返回第一列
x++, y = 0;
if (x == 9)
{
for (int i = 0; i < 9; i++)
cout << g[i] << endl;
return true;//这里返回true让下面for里面中间的dfs直接结束,不在回溯,少枚举很多情况
//并且是输出唯一解
}
if (g[x][y] != '.')
return dfs(x, y + 1);
for (int i = 0; i < 9; i++)
{
if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i])
{
g[x][y] = i + '1';
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
if(dfs(x, y + 1)) return true;//剪枝,dfs返回值是true上面输出了答案,不用再回溯
//并且这一枝递归直接结束。
g[x][y] = '.';//回溯
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;//回溯
}
}
return false;//如果某个方案失败,需要返回false让上面回溯
//不加这个也不会输出结果,因为如果这一枝递归没成功,返回false上面才能回溯
}
int main()
{
for (int i = 0; i < 9; i++)
{
cin >> g[i];
for (int j = 0; j < 9; j++)
{
if (g[i][j] != '.')
{
int t = g[i][j] - '1';
row[i][t] = col[j][t] = cell[i / 3][j / 3][t] = true;
}
}
}
dfs(0, 0);
return 0;
}
法二:未剪枝(时间复杂度高)
bool dfs(int x, int y)
{
if (y == 9) x ++, y = 0;
if (x == 9)
{
for (int i = 0; i < 9; i ++ ) cout << g[i] << endl;
return true;
}
if (g[x][y] != '.') return dfs(x, y + 1);
for (int i = 0; i < 9; i ++ )
if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i])
{
g[x][y] = i + '1';
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
dfs(x, y + 1);//不同点
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;
g[x][y] = '.';
}
//没有返回值
}
tips:改成void类型时没输出结果过原因
错误代码 dfs(x, y+1)--------->如果当前位是数字,就直接往后搜,不能执行后面的for循环对该数进行赋值,否则就改变了该位置上原来的数值,也就搜不出结果
###也就是说,改成bool类型后,当g[x][y]!= '.'时,是直接dfs(x, y + 1)后面的for不执行,而void,是dfs(x, y + 1)后,后面那块for还执行
13.BFS
eg1:1101. 献给阿尔吉侬的花束
思路:求最短路-------->BFS
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 210;
typedef pair<int, int> pii;
int n, m, r, c;
char g[N][N];
int d[N][N];
pii q[N * N];
int stx, sty;
int bfs()
{
int hh = 0, tt = 0;
q[0] = {stx, sty};//插入起点
memset(d, -1, sizeof d);//初始化所有点都未走过
d[stx][sty] = 0;//初始化
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
while (hh <= tt)
{
auto t = q[hh++];
for (int i = 0; i < 4; i++)
{
int x = t.first + dx[i], y = t.second + dy[i];
if (x >= 0 && x < r && y >= 0 && y < c && g[x][y] != '#' && d[x][y] == -1)
{
d[x][y] = d[t.first][t.second] + 1;
if (g[x][y] == 'E')
return d[x][y];
q[++tt] = {x, y};
}
}
}
return -1;
}
int main()
{
cin >> n;
while (n--)
{
cin >> r >> c;
for (int i = 0; i < r; i++)
for (int j = 0; j < c; j++)
{
cin >> g[i][j];
if (g[i][j] == 'S')
stx = i, sty = j;
}
int t = bfs();
if (t == -1)
cout << "oop!" << endl;
else
cout << t << endl;
}
return 0;
}
14.树与图遍历
eg1: 3699. 树的高度
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int res = 0;
int h[N], e[N * 2], ne[N * 2], idx, d[N];//无向图---->开两倍
bool st[N];
void add(int a, int b)//存储边
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
st[u] = true;
res = max(res, d[u]);
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
d[j] = d[u] + 1;
dfs(j);
}
}
}
int main()
{
idx = 0;
memset(h, -1, sizeof h);
cin >> n >> m;
d[m] = 0;
for (int i = 0; i < n - 1; i++)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);//无向图
}
dfs(m);
cout << res << endl;
return 0;
}
15.拓扑排序
tips:有向无环图<-------->拓扑序列
eg1:3704. 排队
思路:
- 每个要求包含两个整数 a,b,表示小朋友 a 要排在小朋友 b 的前面。--->拓扑排序
- 当符合条件的排队顺序不唯一时,编号更小的小朋友尽量更靠前。---->优先队列
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 510, M = 5010;
int n, m;
int h[N], e[M], ne[M], idx, d[N];//d[i]存i的入度
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void topsort()
{
priority_queue<int, vector<int>, greater<int>> heap;//优先队列
for (int i = 1; i <= n; i++)
if (!d[i])
heap.push(i);
while (heap.size())
{
auto t = heap.top();
printf("%d ", t);
heap.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (--d[j] == 0)
heap.push(j);
}
}
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
while (m--)
{
int a, b;
cin >> a >> b;
d[b]++;//a在b的前面,a-->b,b的入度+1
add(a, b);
}
topsort();
return 0;
}
16.最短路
eg1:4275. Dijkstra序列
思想:朴素版Dijkstra
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int g[N][N], dist[N];
bool st[N];
int q[N];
bool dijkstra()
{
memset(dist, 0x3f, sizeof dist);
memset(st,0,sizeof st);
dist[q[0]] = 0;
for (int i = 0; i < n;i++)
{
int t = q[i];
for (int j = 1; j <= n;j++)
{
if(!st[j]&&dist[t]>dist[j])
return false;
}
for (int j = 1; j <= n;j++)
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
return true;
}
int main()
{
scanf("%d%d", &n, &m);
memset(g,0x3f,sizeof g);
while (m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = g[b][a] = c;
}
int k;
scanf("%d", &k);
while(k--)
{
for (int i = 0; i < n;i++)
scanf("%d", &q[i]);
if(dijkstra())
puts("Yes");
else
puts("No");
}
return 0;
}
eg2: 4196. 最短路径
思想:spfa
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
typedef long long ll;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int pre[N];//记录路径
bool st[N];
ll dist[N];
int path[N];//输出路径
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa()//从起点遍历到终点
{
memset(dist, 0x3f, sizeof dist);//初始化
memset(pre, -1, sizeof pre);//初始化
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
pre[j] = t;//记录路径
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h,-1,sizeof h);//初始化邻接表
while (m--)
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
add(a, b, w), add(b, a, w);
}
spfa();
if (pre[n] == -1)
puts("-1");
else
{
int cnt = 0;
for (int i = n; i != -1; i = pre[i])//从终点向起点存储路径
path[cnt++] = i;
for (int i = cnt - 1; i >= 0; i--)//逆序输出路径
printf("%d ", path[i]);
}
return 0;
}
eg3:1488. 最短距离
思想:超级源点+ 堆优化版Dijkstra
超级源点跟超级汇点是模拟出来的虚拟点,多用于图中:
<1>同时有多个源点和多个汇点,建立超级源点和超级汇点
<2>同时有多个源点和一个汇点,建立超级源点
<3>同时有多个汇点和一个源点,建立超级汇点
思路:
- 本题为多个源点的最短路问题-------->对每个源点使用Dijkstra(TLE)
- 优化:新增一个点(超级源点),超级源点连向所有商店一条边,权值为0,再从超级源点做单源最短路。
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int N = 1e5 + 10, M = 3e5 + 10;//无向图要开N的(2倍)+新增了超级源点连向各源点的边(1倍)
typedef pair<int, int> pii;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
bool st[N];
int dist[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[0] = 0;//超级源点的下标为0,从超级源点开始做单源最短路
priority_queue<pii, vector<pii>, greater<pii>> heap;
heap.push({0, 0});//同步修改
while(heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(st[ver])
continue;
st[ver] = true;
for (int i = h[ver]; i != -1;i=ne[i])
{
int j = e[i];
if(dist[j]>distance+w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h,-1,sizeof h);
while (m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
int k;
scanf("%d",&k);
while (k--)
{
int x;
scanf("%d", &x);
add(0, x, 0);
}
dijkstra();
int q;
scanf("%d",&q);
while(q--)
{
int y;
scanf("%d", &y);
printf("%d\n", dist[y]);
}
return 0;
}
eg4:4872. 最短路之和
思想:Floyd
小技巧:在图中删点不好做,逆向思维,把点一个一个加入图中,
每次以第 i个点为中间点做 Floyd,最后把所有加入的点两两之间距离的和求出即可。
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 510;
typedef long long ll;
int n;
bool st[N];//判断点是否在图中
int d[N][N];
int p[N];
ll ans[N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
scanf("%d", &d[i][j]);
for (int i = 1; i <= n;i++)
scanf("%d", &p[i]);
for (int u = n; u > 0;u--)//向图中加入点
{
int k = p[u];
st[k] = true;
for (int i = 1; i <= n;i++)//floyd
for (int j = 1; j <= n;j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
for (int i = 1; i <= n;i++)
for (int j = 1; j <= n;j++)
if(st[i]&&st[j])
ans[u] += d[i][j];
}
for (int i = 1; i <= n;i++)
printf("%lld ", ans[i]);
return 0;
}
17.最小生成树
eg1:346. 走廊泼水节
思想:Kruskal
思路:用Kruskal 算法构建最小生成树
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 6010;
int n, m;
int p[N], cnt[N];
struct Edge
{
int a, b, w;
bool operator < (const Edge &W) const
{
return w < W.w;
}
} edges[N];
int find(int x)
{
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
int Kruskal()
{
sort(edges, edges + n - 1);
for (int i = 1; i <= n; i++)
p[i] = i, cnt[i] = 1;
int ans = 0;
for (int i = 0; i < n - 1; i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b)
{
p[a] = b;
ans += (cnt[a] * cnt[b] - 1) * (w + 1);
cnt[b] += cnt[a];
}
}
return ans;
}
int main()
{
int T;
scanf("%d",&T);
while (T--)
{
scanf("%d", &n);
for (int i = 0; i < n - 1; i++)
{
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
edges[i] = {x, y, z};
}
printf("%d\n", Kruskal());
}
return 0;
}
eg2:3728. 城市通电
18.二分图
eg1:257. 关押罪犯
思想:染色法判定二分图
思路:
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 20010, M = 200010;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int color[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u,int c,int limit)
{
color[u] = c;
for (int i = h[u]; i != -1;i=ne[i])
{
if(w[i]<=limit)//该点的权重<=limit,不能构成二分图,继续向下遍历
continue;
int j = e[i];
if(color[j]==-1)
{
if(!dfs(j,!c,limit))
return false;
}
else if (color[j]==c)
return false;
}
return true;
}
bool check(int limit)
{
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n;i++)
{
if(color[i]==-1)
if(!dfs(i,0,limit))
{
flag = false;
break;
}
}
return flag;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e9;
while (l < r)
{
int mid = l + r >> 1;
if(check(mid))
r = mid;
else
l = mid + 1;
}
printf("%d", l);
return 0;
}
eg2:4205. 树的增边
思想:染色法判定二分图(所有的树都是二分图)
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5+10, M = 2*N;
typedef long long ll;
int n, m;
int h[N], e[M], ne[M], idx;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int c, int f)
{
if (!c)//若u这个点为另外一种颜色
m++;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(j==f)//保证一直向下 不可以向上
continue;
dfs(j, !c, u);// 改变颜色 当前点变为父节点
}
}
int main()
{
scanf("%d", &n);
memset( h, -1, sizeof h );
for (int i = 1; i <= n - 1; i++)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);//二分图为无向图
}
dfs(1, 0, 0);//遍历树(假设第一个点为白色)
printf("%lld", (ll)m * (n - m) - (n - 1));
return 0;
}
eg3: 1394. 完美牛棚
思想:最大匹配问题------>匈牙利算法
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 40010;//邻接表存边,200个点,每个点最多200条边(200*200)
int n, m;
int h[N], e[N], ne[N], idx;
int match[N];
bool st[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x)//匈牙利算法
{
for (int i = h[x]; i != -1;i=ne[i])
{
int j = e[i];
if(!st[j])
{
st[j] = true;
if(match[j]==0||find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));
for (int i = 1; i <= n; i++)
{
int cnt;
scanf("%d", &cnt);
while (cnt--)
{
int x;
scanf("%d", &x);
add(i, x);
}
}
int res = 0;
for (int i = 1; i <= n;i++)
{
memset(st, false, sizeof st);
if(find(i))
res++;
}
printf("%d", res);
return 0;
}