ACWing算法提高课笔记
本文为观看了Acwing提高课的笔记,但是由于本人的一些原因,并没有看完,仅更新至图论的欧拉lu
1. 动态规划
从集合角度来考虑DP问题——闫氏思考法
图线型DP
通过所在位置作为状态进行状态计算。
基础:摘花生、最低通行费、数字三角形
进阶:方格取数
思路:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15, M = 2 * N;
int g[N][N];
int dp[M][N][N];
int n;
int main(){
cin >> n;
while(1){
int a, b, c;
cin >> a >> b >> c;
if(a == 0 && b == 0 && c == 0) break;
g[a][b] = c;
}
for(int k = 2; k <= 2 * n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
int j1 = k - i, j2 = k - j;
if(j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n){
int t = g[i][j1];
if(j1 != j2) t += g[j][j2];
int &x = dp[k][i][j];
x = max(x, dp[k - 1][i - 1][j - 1] + t);
x = max(x, dp[k - 1][i - 1][j] + t);
x = max(x, dp[k - 1][i][j - 1] + t);
x = max(x, dp[k - 1][i][j] + t);
}
}
}
}
cout << dp[2 * n][n][n] << endl;
return 0;
}
最长上升子序列模型(LIS)
怪盗基德的滑翔翼
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int a[N], dp[N];
int main()
{
int T;
cin >> T;
while(T --){
memset(dp, 0, sizeof dp);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
// 正项求解LIS问题;
int res = 0;
for(int i = 1; i <= n; i ++){
dp[i] = 1;
for(int j = 1; j < i; j ++){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
for(int i = 1; i <= n; i ++){
res = max(res, dp[i]);
}
for(int i = n; i >= 1; i --){
dp[i] = 1;
for(int j = n; j > i; j --){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
for(int i = 0; i < n; i ++){
res = max(res, dp[i]);
}
cout << res << endl;
}
return 0;
}
登山
合唱队形
思想
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int a[N], dp[N], dp1[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
// 正项求解LIS问题;
for(int i = 1; i <= n; i ++){
dp[i] = 1;
for(int j = 1; j < i; j ++){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
for(int i = n; i >= 1; i --){
dp1[i] = 1;
for(int j = n; j > i; j --){
if(a[i] > a[j]){
dp1[i] = max(dp1[i], dp1[j] + 1);
}
}
}
int res = 0;
for(int i = 1; i <= n; i ++) res = max(res, dp[i] + dp1[i] - 1);
cout << res << endl;
return 0;
}
友好城市
思路:
建立结构体,对河的某一个岸的城市进行排序,对于另一岸的城市来说,就是求最长上升子序列了,因为如果不是上升的子序列的话,一定会有交叉的情况出现。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10;
int n;
PII a[N];
int dp[N], q[N];
int main()
{
cin >> n ;
for(int i = 0; i < n; i ++){
int l, r;
cin >> l >> r;
a[i] = {l, r};
}
sort(a, a + n);
int len = 0;
q[0] = - 2e9;
for(int i = 0; i < n; i ++){
int l = 0, r = len;
while(l < r){
int mid = l + r + 1>> 1;
if(q[mid] < a[i].second) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i].second;
}
cout << len << endl;
return 0;
}
最大上升子序列和
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e4 + 10;
int n;
int a[N], dp[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
// 正项求解LIS问题;
for(int i = 1; i <= n; i ++){
dp[i] = a[i];
for(int j = 1; j < i; j ++){
if(a[i] > a[j]){
dp[i] = max(dp[i], dp[j] + a[i]);
}
}
}
int res = 0;
for(int i = 1; i <= n; i ++) res = max(res, dp[i]);
cout << res << endl;
return 0;
}
导弹拦截
思想:
第一行显然是求一个最长不上升子序列
第二行通过贪心的分析,即对于每个不上升序列,将第i个导弹放置在会导致下降幅度最低的序列中,如果大于任何一个序列的最后一个值,则新开一个不上升序列去储存。最终得到了一个不上升序列组,组数就是导弹防御系统套数,那么可以看到。这个不上升序列组是和利用贪心算法得到的上升子序列个数是一样的。因此可以直接使用最长上升子序列个数进行计算。
#include<iostream>
using namespace std;
const int N = 1010;
int a[N], dp[N];
int n;
int main(){
while(cin >> a[n]) n ++;
for(int i = 0; i < n; i ++){
dp[i] = 1;
for(int j = 0; j < i; j ++){
if(a[i] <= a[j]) dp[i] = max(dp[i], dp[j] + 1);
}
}
int res = 0;
for(int i = 0; i < n; i ++){
res = max(res, dp[i]);
}
cout << res << endl;
for(int i = 0; i < n; i ++){
dp[i] = 1;
for(int j = 0; j < i; j ++){
if(a[i] > a[j]) dp[i] = max(dp[i], dp[j] + 1);
}
}
res = 0;
for(int i = 0; i < n; i ++) res = max(res, dp[i]);
cout << res << endl;
}
导弹防御系统
思想:
本题和上题唯一的区别就在于可以有两种序列,上升和下降。那么新引入的一个数就得考虑是在上升序列还是下降序列中,因此代码采用暴搜,对于当前的i这个数,利用回溯法来搜索其在上升和下降序列中的最小值。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 55;
int n;
int q[N];
int up[N], down[N];
int ans;
void dfs(int u, int su, int sd){//当前枚举到了第几个数,当前的上升子序列个数,当前的下降子序列个数。
if(su + sd >= ans) return;
if(u == n){
ans = su + sd;
return;
}
//情况1,把当前数据放到上升子序列当中。
int k = 0;
while(k < su && up[k] >= q[u]) k ++;
int t = up[k];
up[k] = q[u];
dfs(u + 1, max(k + 1, su), sd);
up[k] = t;
//情况2,把当前数据放到下降子序列当中。
k = 0;
while(k < sd && down[k] <= q[u]) k ++;
t = down[k];
down[k] = q[u];
dfs(u + 1, su, max(sd, k + 1));
down[k] = t;
}
int main(){
while(cin >> n, n){
for(int i = 0; i < n; i ++) cin >> q[i];
ans = n;
dfs(0, 0, 0);
cout << ans << endl;
}
return 0;
}
最长公共上升子序列
思想:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3010;
int a[N], b[N], dp[N][N];
int n, m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = 1; i <= m; i ++) cin >> b[i];
//未优化版
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
dp[i][j] = dp[i - 1][j];
if(a[i] == b[j]){
dp[i][j] = max(dp[i][j], 1);
for(int k = 1; k < j; k ++){
if(b[j] > b[k]){
dp[i][j] = max(dp[i][k] + 1, dp[i][j]);
}
}
}
}
}
//优化版
for(int i = 1; i <= n; i ++){
int maxv = 1;//由于b[j]和b[k]的关系可以转化为a[i]和b[k]的关系,因此在这里利用maxv来一直存储从1到j中dp[i][j]的最大值。
for(int j = 1; j <= m; j ++){
dp[i][j] = dp[i - 1][j];
if(a[i] == b[j]) dp[i][j] = max(dp[i][j], maxv);
if(b[j] < a[i]) maxv = max(dp[i][j] + 1, maxv);
}
}
int res = 0;
for(int i = 1; i <= m; i ++) res = max(dp[n][i], res);
cout << res << endl;
return 0;
}
背包问题(续讲)
多重背包问题(单调队列优化(注意常数项的处理))
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e4 + 10;
int n, m;
int f[N], g[N], q[N];//g是用来存储上一层的f的。
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i ++){
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof f);
for(int j = 0; j < v; j ++){//j代表着k%v后等于多少。
int hh = 0, tt = -1;
for(int k = j; k <= m; k += v){
if(hh <= tt && q[hh] < k - s * v) hh ++;//单调队列中k - s * v 如果大于q[hh]这个端点时,就让hh++。
if(hh <= tt) f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);//f[k]等于f[k]和前s*v中最大的一个g[q[hh]]+距离/v*w的最大值,
while(hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt --;//如果当前的q[tt]的值<g[k]的值(注意这里进行了常数项的处理,让g[j]等于其本身的g[j]而没有加常数。因为这里是同一行进行比较)。(q[tt]!= k - v是因为不一定在单调队列中的是连续的。)
q[++ tt] = k;
}
}
}
cout << f[m] << endl;
return 0;
}
装箱问题(01背包)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20010;
int n, V;
int dp[N], v[N];
int main(){
cin >> V >> n;
for(int i = 0; i < n; i ++) cin >> v[i];
for(int i = 0; i < n; i ++){
for(int j = V; j >= v[i]; j --){
dp[j] = max(dp[j], dp[j - v[i]] + v[i]);
}
}
cout << V - dp[V] << endl;
return 0;
}
二维01背包问题
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int v1[N], v2[N], dp[N][N];
int n, v, m;
int main(){
cin >> n >> v >> m;
for(int i = 0; i < n; i ++){
int v1, v2, w;
cin >> v1 >> v2 >> w;
for(int j = v; j >= v1; j --){
for(int k = m; k >= v2; k --){
dp[j][k] = max(dp[j - v1][k - v2] + w, dp[j][k]);
}
}
}
cout << dp[v][m] << endl;
return 0;
}
宠物小精灵之收服(二维费用01背包问题)
将01背包中的一维限制空间改为二维的,血量和数量的限制。从0到m遍历dp[n][i],当遇到了i使得dp[n][i]==dp[n][m],就说明了这个i就是最小的收服气血。计算剩余的气血。最终输出即可
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N][N];
int n, m, k;
int num[N], hurt[N];
int main(){
cin >> n >> m >> k;
for(int i = 0; i < k; i ++) cin >> num[i] >> hurt[i];
for(int i = 0; i < k; i ++){
for(int j = n; j >= num[i]; j --){
for(int l = m - 1; l >= hurt[i]; l --){
dp[j][l] = max(dp[j][l], dp[j - num[i]][l - hurt[i]] + 1);
}
}
}
int res = 0;
for(int i = 0; i < m; i ++){
if(dp[n][i] == dp[n][m - 1]){
res = i;
break;
}
}
cout << dp[n][m - 1] << " " << m - res << endl;
}
潜水员(二维01背包问题)
思路
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 10010;
int a[N], dp[M];//前i个数的和恰好为j的方案个数。
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
dp[0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = m; j >= a[i]; j --){
dp[j] += dp[j - a[i]];
}
}
cout << dp[m] << endl;
return 0;
}
庆功会(完全和多重背包模板一样)
#include<iostream>
using namespace std;
const int N = 6010;
int n, V;
int v[N], w[N], s[N];
int dp[N][N];
int main(){
cin >> n >> V;
for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= V; j ++){
dp[i][j] = dp[i - 1][j];
//朴素做法,在完全背包问题中加入一个数量小于s[i]的限制即可
for(int k = 0; j - k * v[i] >= 0 && k <= s[i]; k ++){
dp[i][j] = max(dp[i][j],dp[i - 1][j - k * v[i]]+ k * w[i]);
}
}
}
cout << dp[n][V] << endl;
return 0;
}
买书(完全背包问题)(这里是恰好得全部花完)
思路:
由于是完全背包问题,和买书的前后没有任何关系,只在意有没有买当前书,和买了多少,因此设dp[i][j]为前i本书共花费j元的方案数,那么状态划分就是dp[i][j] = dp[i - 1][j]+dp[i][j - v[i]],包含了和没包含当前的书,没包含当前书就是dp[i - 1][j]。包含了当前书就是区别在买了几本,由于买了j - 2v 是包含在j - v 中的,因此不管买了几本,买了就是dp[i][j - v],因此是dp[i][j] = dp[i - 1][j]+dp[i][j - v[i]]。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N][N];//前i本书,花费j元。
int n, V;
int v[5] = {0, 10, 20, 50, 100};
int main()
{
cin >> V;
n = 4;
dp[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= V; j ++){
dp[i][j] = dp[i - 1][j];
if(j >= v[i]) dp[i][j] = dp[i][j - v[i]];
}
}
cout << dp[4][V] << endl;
return 0;
}
机器分配(分组背包+具体方案数(字典序倒着选))
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20;
int s[N][N];
int dp[N][N];//前i个公司总共分配j台的利润。
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
cin >> s[i][j];
}
}
for(int i = n; i >= 1; i --){
for(int j = 1; j <= m; j ++){
for(int k = 0; k <= j; k ++){
dp[i][j] = max(dp[i][j], dp[i + 1][j - k] + s[i][k]);
}
}
}
cout << dp[1][m] << endl;
int i = 1, j = m;
while(i <= n){
for(int k = j; k >= 0; k --){
if((dp[i][j] - dp[i + 1][k]) == s[i][j - k]){
cout << i << " " << j - k << endl;
j = k;
break;
}
}
i ++ ;
}
return 0;
}
金明预算方案(有依赖的背包问题+状态压缩)
思路:
把有依赖的背包问题分为每个组,把每个组所有状态表示出来,注意主件是必须选择的。然后变成分组背包问题即可
#include <iostream>
#include <cstring>
#include <algorithm>
#define v first
#define w second
using namespace std;
typedef pair<int, int> PII;
const int N = 70, M = 32010;
int n, m;
PII master[N];
vector<PII> servent[N];
int dp[M];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i ++){
int v, w, q;
cin >> v >> w >> q;
if(!q) master[i] = {v, v * w};
else servent[q].push_back({v, v * w});//q是所属附件的编号
}
for(int i = 1; i <= n; i ++){
if(master[i].v){//如果有主键
for(int j = m; j >= 0; j --){//金额限制
auto &sv = servent[i];
for(int k = 0; k < 1 << sv.size(); k ++){//枚举有哪些从件。
int v = master[i].v, w = master[i].w;
for(int u = 0; u < sv.size(); u ++){//计算这种情况下的从件的体积和价值和。
if(k >> u & 1){
v += sv[u].v;
w += sv[u].w;
}
}
if(j >= v) dp[j] = max(dp[j], dp[j - v] + w);
}
}
}
}
cout << dp[m] << endl;
return 0;
}
货币系统(完全背包问题)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N =16, M = 3010;
typedef long long LL;
int w[N];
LL dp[M];
int n, m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> w[i];
dp[0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = w[i]; j <= m; j ++){
dp[j] += dp[j - w[i]];
}
}
cout << dp[m] << endl;
return 0;
}
货币系统(完全背包问题加模拟)
思路
将a数组排序,从小到大选择a[i]进入b数组,当且仅当a[i]不能被a中前i-1项表示时进入b数组。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 25010;
int a[N];
int dp[M];
int n, t;
int b[N];
int main()
{
cin >> t;
while(t --){
cin >> n;
for(int i = 0; i < n; i ++) cin >> a[i];
sort(a, a + n);
int m = a[n - 1];
memset(dp, 0, sizeof dp);
int cnt = 0;
dp[0] = 1;
for(int i = 0; i < n; i ++){
if(!dp[a[i]]) cnt ++;//判断当前dp[a[i]]是否存在,放在前面的原因是用前i-1个a来构成a[i]。
for(int j = a[i]; j <= m; j ++){
dp[j] += dp[j - a[i]];
}
}
cout << cnt << endl;
}
return 0;
}
混合背包问题(01,完全,多重)
思路:
将多重背包通过二进制转化为01背包问题,最后分成01和完全背包问题来做。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N];
int n, V;
int main(){
cin >> n >> V;
for(int i = 0; i < n; i ++){
int v, w ,s;
cin >> v >> w >> s;
if(s == 0){
for(int j = v; j <= V; j ++) dp[j] = max(dp[j - v] + w, dp[j]);
}
else{//把多重背包问题通过二进制话变为01背包问题放在一起来求。
if(s == -1) s = 1;
for(int k = 1; k <= s; k *= 2){
for(int j = V; j >= k * v; j --){
dp[j] = max(dp[j], dp[j - k * v] + k * w);
}
s -= k;
}
if(s){
for(int j = V; j >= s * v; j --){
dp[j] = max(dp[j], dp[j - s * v] + s * w);
}
}
}
}
cout << dp[V] << endl;
return 0;
}
有依赖的背包问题
思路:
对于当前结点,枚举其儿子结点能够利用的空间的情况(保留了自己结点的空间)下的最大值
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N];
int h[N], ne[N], e[N], idx;
int dp[N][N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//没必要纠结于分组背包,这个可以直接当成树形dp做就好。
void dfs(int u){
for(int i = h[u]; i != -1; i = ne[i]){
int son = e[i];
dfs(son);//先递归儿子结点再处理自己,自下而上的方法。
//分组背包(对于各个儿子按照空间进行分配,比如给这个儿子分配多少空间,给这个儿子分配了所有空间的情况再到第二个儿子那里在这些情况上进行更新)
for(int j = m - v[u]; j >= 0; j --){
for(int k = 0; k <= j; k ++){
dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[son][k]);
}
}
}
for(int i = m; i >= v[u]; i --) dp[u][i] = dp[u][i - v[u]] + w[u];//加上自己的情况。
for(int i = 0; i < v[u]; i ++) dp[u][i] = 0;//将分配空间情况小于自己结点的设为0。
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
int root;
for(int i = 1; i <= n; i ++){
int p;
cin >> v[i] >> w[i] >> p;
if(p == -1) root = i;
else add(p, i);
}
dfs(root);
cout << dp[root][m] << endl;
return 0;
}
能量石(贪心+01背包)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 10010;
int n, t;
struct Stone{
int s, e, l;
bool operator< (Stone t){
return s * t.l < t.s * l;
}
}stone[N];
int dp[M];//前i个石头中选,时间恰好为j的方案
int main()
{
cin >> t;
int q = t;
while(t --){
cin >> n;
int m = 0;
for(int i = 0; i < n; i ++){
int e, s, l;
cin >> s >> e >> l;
stone[i] = {s, e, l};
m += s;
}
sort(stone, stone + n);
memset(dp, -0x3f, sizeof dp);
dp[0] = 0;
for(int i = 0; i < n; i ++){
int s = stone[i].s, e = stone[i].e, l = stone[i].l;
for(int j = m; j >= s; j --){
dp[j] = max(dp[j], dp[j - s] + e - (j - s) * l);
}
}
int res = 0;
for(int i = 0; i <= m; i ++) res = max(res, dp[i]);
cout << "Case #" << q - t << ": " << res << endl;
}
return 0;
}
状态机模型
大盗阿福
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int t, n;
int dp[N][2];//0表示偷,1表示不偷。
int a[N];
int main(){
cin >> t;
while(t --){
cin >> n;
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = 1; i <= n; i ++){
dp[i][0] = dp[i - 1][1] + a[i];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0]);
}
cout << max(dp[n][0], dp[n][1]) << endl;
}
return 0;
}
股票交易IV(限制了交易次数)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = 110, INF = 1e9;
int dp[N][M][2];//0表示持有股票,1表示未持有股票
int n, k;
int a[N];
int main(){
cin >> n >> k;
for(int i = 1; i <= n; i ++) cin >> a[i];
memset(dp, -0x3f, sizeof dp);
for(int i = 0; i <= n; i ++) dp[i][0][1] = 0;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= k; j ++){
dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j - 1][1] - a[i]);//当前持有股票且为第j次交易从之前持有股票或者之前未持有股票且进行了第j-1次交易来。因为买入卖出算一次交易,所以买入了就算下一次交易了。
dp[i][j][1] = max(dp[i - 1][j][0] + a[i], dp[i - 1][j][1]);
}
}
int res = 0;
for(int i = 0; i <= k; i ++){
res = max(res, dp[n][i][0]);
}
cout << res << endl;
}
股票交易V(增加了冷冻期)
#include <iostream>
using namespace std;
const int N = 1e5 + 10, INF = 1e9;
int a[N];
int dp[N][3];//0表示持有股票,1表示冷冻期,2表示未持有股票但可以购买。
int n;
int main(){
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
dp[0][0] = dp[0][1] = -INF;
for(int i = 1; i <= n; i ++){
dp[i][0] = max(dp[i - 1][2] - a[i], dp[i - 1][0]);
dp[i][1] = dp[i - 1][0] + a[i];
dp[i][2] = max(dp[i - 1][1], dp[i - 1][2]);
}
cout << max(dp[n][1], dp[n][2]) << endl;
}
设计密码(KMP+状态机DP)
思想
利用kmp字符串在哪里作为状态。
dp[i][j]可以由dp[i - 1][u]这个状态转化而来,j表示在kmp字符串中处于第几位。比如u=10,但是下一个字符导致u从10到了5,就有dp[i][5] += dp[i - 1][10],最终只需要i=n(代表n个字符)j!=m(代表最终结果没有包含子串)的结果即可。因此只需要枚举每一个状态是由上一个状态哪里来的即可。
#include <iostream>
#include <cstring>
#include <string.h>
using namespace std;
const int N = 60, mod = 1e9 + 7;
int dp[N][N];
int n, m;
char str[N];
int kmp[N];
int main(){
cin >> n >> str + 1;
m = strlen(str + 1);
for(int i = 2, j = 0; i <= m; i ++, j ++){
while(j & str[i] != str[j + 1]) j = kmp[j];
if(str[i] == str[j + 1]) j ++;
kmp[i] = j;
}
dp[0][0] = 1;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
for(char k = 'a'; k <= 'z'; k ++){
int u = j;
while(u & k != str[u + 1]) u = kmp[u];
if(k == str[u + 1]) u ++;
if(u < m) dp[i + 1][u] = (dp[i + 1][u] + dp[i][j]) % mod;
}
}
}
int res = 0;
for(int i = 0; i < m; i ++){
res = (res + dp[n][i]) % mod;
}
cout << res << endl;
return 0;
}
AC自动机
状态压缩DP
小国王
#include<iostream>
#include <vector>
using namespace std;
const int N = 12, M = N * N, L = 1 << N;
typedef long long LL;
LL dp[N][M][L];
int n, m;
vector<int> state;
vector<int> head[M];
bool check(int x){
int cnt = 0;
while(x){
if(x & 1) cnt ++;
else cnt = 0;
x = x >> 1;
if(cnt > 1) return false;
}
return true;
}
int count(int x){
int cnt = 0;
while(x){
if(x & 1) cnt ++;
x >>= 1;
}
return cnt;
}
int main(){
cin >> n >> m;
//先预处理哪些是有效的状态
for(int i = 0; i < 1 << n; i ++){
if(check(i)){
state.push_back(i);
}
}
for(int i = 0; i < state.size(); i ++){
for(int j = 0; j < state.size(); j ++){
int a = state[i], b = state[j];
if(((a >> 1) & b) == 0 && ((a << 1) & b) == 0 && (a & b) == 0){
head[i].push_back(j);
}
}
}
dp[0][0][0] = 1;
for(int i = 1; i <= n + 1; i ++){
for(int j = 0; j <= m; j ++){
for(int a = 0; a < state.size(); a ++){
for(int b : head[a]){
int c = count(state[a]);
if(j >= c) dp[i][j][a] += dp[i - 1][j - c][b];
}
}
}
}
cout << dp[n + 1][m][0] << endl;
return 0;
}
玉米田
#include <iostream>
#include <cmath>
using namespace std;
const int N = 15, M = 1 << N, mod = 1e8;
int dp[N][M];
int n, m;
int a[N];
bool check(int y, int x){
if(x & a[y]) return false;
int cnt = 0;
while(x){
if(x & 1) cnt ++;
else cnt = 0;
x >>= 1;
if(cnt > 1) return false;
}
return true;
}
int main(){
cin >> n >> m;
for(int j = 0; j < n; j ++){
for(int i = 1; i <= m; i ++){
int b;
cin >> b;
a[i] += abs(1 - b) * pow(2, j);
}
}
dp[0][0] = 1;
for(int i = 1; i <= m + 1; i ++){
for(int j = 0; j < 1 << n; j ++){
for(int k = 0; k < 1 << n; k ++){
if((j & k) == 0 && check(i, j) && check(i - 1, k)){
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;
}
}
}
}
cout << dp[m + 1][0] << endl;
return 0;
}
炮兵征地(前两列对当前状态有影响)
思路
多增加一维用于枚举前一行的状态,最终以两行两行状态进行推进。
#include<iostream>
#include<cstring>
#include<algorithm>
#include <cmath>
using namespace std;
const int N = 11, M = 1 << 10;
int n, m;
int g[110];
vector<int> state;
int dp[2][M][M];
int cnt[M];
bool check(int x){
for(int i = 0; i < m; i ++){
if(((x >> i) & 1) && (((x >> (i + 1)) & 1) || ((x >> (i + 2)) & 1))) return false;
}
return true;
}
int count(int x){
int cnt = 0;
while(x){
if(x & 1) cnt ++;
x >>= 1;
}
return cnt;
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
for(int j = 0; j < m; j ++){
char c;
cin >> c;
if(c == 'H') g[i] += 1 << j;
}
}
for(int i = 0; i < 1 << m; i ++){
if(check(i)){
state.push_back(i);
cnt[i] = count(i);
}
}
//与第几行放在外面没关系,重要的是各行确定后之间的关系。
for(int i = 1; i <= n + 2; i ++){
for(int j = 0; j < state.size(); j ++){//i-1
for(int k = 0; k < state.size(); k ++){//i
for(int u = 0; u < state.size(); u ++){//i-2
int a = state[j], b = state[k], c = state[u];
if((a & b) | (a & c) | (b & c)) continue;
if((g[i - 1] & a) | (g[i] & b)) continue;
dp[i & 1][j][k] = max(dp[i - 1 & 1][u][j] + cnt[b], dp[i & 1][j][k]);
}
}
}
}
cout << dp[n + 2 & 1][0][0] << endl;
}
愤怒的小鸟
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 18, M = 1 << 18;
const double eps = 1e-8;
int n, m;
PDD q[N];
int path[N][N];
int dp[M];
int cmp(double x, double y){
if(fabs(x - y) < eps) return 0;
if(x < y) return -1;
return 1;
}
int main(){
int t;
cin >> t;
while(t --){
cin >> n >> m;
for(int i = 0; i < n; i ++) cin >> q[i].x >> q[i].y;//读取每个点的位置。
memset(path, 0, sizeof path);
for(int i = 0; i < n; i ++){//对于所有的两个点进行路径判断。
path[i][i] = 1 << i;
for(int j = 0; j < n; j ++){
double x1 = q[i].x, y1 = q[i].y;
double x2 = q[j].x, y2 = q[j].y;
if(!cmp(x1, x2)) continue;//如果这两个点是垂直的就不要,重新选点。
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;//否则计算出路径。
if(cmp(a, 0) >= 0) continue;//这里竟然能被卡。。得用浮点数比较。。
int state = 0;//设置好当前的情况。
for(int k = 0; k < n; k ++){//如果对于某个点来说,能够被这个路径给覆盖
double x = q[k].x, y = q[k].y;
if(!cmp(a * x * x + b * x, y)) state += 1 << k;//就把这个点加到这个情况里包括自己。(自己带进去肯定成立的)。
}
path[i][j] = state;//然后记录这条i和j的路径最终能够覆盖多少人。
}
}
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
for(int i = 0; i + 1 < 1 << n; i ++){//状态模拟(所有选择的方案)
int x = 0;
for(int j = 0; j < n; j ++){
if(!((i >> j) & 1)){//如果i这个状态中j点还没有选择,就让x=j,同时break。
x = j;
break;
}
}
for(int j = 0; j < n; j ++){//让所有x可以别的点组成点状态和i的或作为下一个状态,由其本身和i这个状态加一条直线(x,j)的最小值为他。
dp[i | path[x][j]] = min(dp[i | path[x][j]], dp[i] + 1);
}
}
cout << dp[(1 << n) - 1] << endl;//所有都选择的情况。
}
return 0;
}
环形石子合并:
对于一个环形的结构,常规做法:我们求最优解的时候,通常将数组变为原先的两倍,即将数组复制一次,然后循环判断哪个区间是最优的解。
#include<iostream>
#include<math.h>
#include<cstring>
using namespace std;
const int N = 210, INF = 1e9;
int a[N], s[N];
int dp[N][N], f[N][N];//从i到j堆石子合并的代价
int n;
int main(){
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = n + 1; i <= 2 * n; i ++) a[i] = a[i - n];
for(int i = 1; i <= 2 * n; i ++) s[i] = s[i-1] + a[i];
memset(dp, 0x3f, sizeof dp);
memset(f, -0x3f, sizeof f);
for(int len = 1; len <= 2 * n; len ++){
for(int i = 1; i + len - 1 <= 2 * n; i ++){
int j = i + len - 1;
if(j == i) dp[i][j] = f[i][j] = 0;
else{
for(int k = i; k <= j; k ++){
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + s[j] - s[i - 1]);
f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
// if(i == j - 1) cout << "i = " << i << " j = " << j << " f = " << f[i][j] << endl;
}
}
}
}
int res = INF;
for(int i = 1; i <= n;i ++){
res = min(res, dp[i][i + n - 1]);
}
cout << res << endl;
res = -INF;
for(int i = 1; i <= n;i ++){
res = max(res, f[i][i + n - 1]);
}
cout << res << endl;
}
能量项链
思想
和石子合并几乎一模一样,只是加的东西从s[j]-s[i-1]变成了a[i].first*a[k].second*a[j].second
#include<iostream>
#include<math.h>
#include<cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 110, INF = 1e9;
int f[N][N];//从i到j堆石子合并的代价
int n;
PII a[N];
int main(){
cin >> n;
for(int i = 1; i <= n; i ++){
int b;
cin >> b;
a[i].first = b;
a[i - 1].second = b;
}
a[n].second = a[1].first;
for(int i = n + 1; i <= 2 * n; i ++) a[i] = a[i - n];
memset(f, -0x3f, sizeof f);
for(int len = 1; len <= n; len ++){
for(int i = 1; i + len - 1 <= 2 * n; i ++){
int j = i + len - 1;
if(j == i) f[i][j] = 0;
else{
for(int k = i; k <= j; k ++){
f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + a[i].first * a[k].second * a[j].second);
}
}
}
}
int res = -INF;
for(int i = 1; i <= n; i ++){
res = max(res, f[i][i + n - 1]);
}
cout << res << endl;
}
凸多边型的划分(高精度+区间DP)
思路:
对于任意距离大于2的点,可以和其中间的点形成三角形,并且将剩余部分隔开。因此dp[i][j] = dp[i][k] + dp[k][j] + w[i] * w[k] * w[l]。
#include<iostream>
#include<math.h>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 55, M = 35, INF = 1e9;
LL f[N][N][M];//从i到j堆石子合并的代价
int n;
LL a[N];
void add(LL a[], LL b[]){
LL c[M];
LL t = 0;
memset(c, 0, sizeof c);
for(int i = 0; i < M; i ++){
t += a[i] + b[i];
c[i] = t % 10;
t /= 10;
}
memcpy(a, c, sizeof c);
}
void mul(LL a[], LL b){
LL t = 0;
LL c[M];
memset(c, 0, sizeof c);
for(int i = 0; i < M; i ++){
t += a[i] * b;
c[i] = t % 10;
t /= 10;
}
memcpy(a, c, sizeof c);
}
int cmp(LL a[], LL b[]){
for(int i = M - 1; i >= 0; i --){
if(a[i] < b[i]) return -1;
else if(a[i] > b[i]) return 1;
}
return 0;
}
void print(LL a[]){
int flag = 0;
for(int i = M - 1; i >= 0; i --){
if(a[i] != 0 || flag){
cout << a[i];
flag = 1;
}
}
}
int main(){
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
LL temp[M];
for(int len = 3; len <= n; len ++){
for(int i = 1; i + len - 1 <= n; i ++){
int j = i + len - 1;
f[i][j][M - 1] = 1;
for(int k = i + 1; k < j; k ++){
memset(temp, 0, sizeof temp);
temp[0] = a[i];
mul(temp, a[k]);
mul(temp, a[j]);
add(temp, f[i][k]);
add(temp, f[k][j]);
if(cmp(f[i][j], temp) > 0){
memcpy(f[i][j], temp, sizeof temp);
}
}
}
}
print(f[1][n]);
}
加分二叉树
思路:
这里的区间dp很妙,表示从i到j区间段的加分最大值,k代表着结点。由于是中序遍历,那么左边就是左子树的加分最大值,右边就是右子树的加分最大值,注意判断子树为空时值为1,子树只有自己的时候就是其本身。由于本题还需要前序输出,因此需要设置g[i][j]用于储存i到j段时最大加分根结点位置,然后输出并递归左区域、右区域。
#include <iostream>
using namespace std;
const int N = 40, INF = 1e9 ;
int w[N];
int dp[N][N], g[N][N];
int n;
void print(int l, int r){
int k = g[l][r];
cout << k << " ";
if(l <= k - 1) print(l, k - 1);
if(r >= k + 1) print(k + 1, r);
}
int main(){
cin >> n ;
for(int i = 1; i <= n; i ++) cin >> w[i];
for(int len = 1; len <= n; len ++){
for(int l = 1; l + len - 1 <= n; l ++){
int r = l + len - 1;
if(l == r){
dp[l][r] = w[l];
g[l][r] = l;
}
else{
for(int k = l; k <= r; k ++){
int left = k == l ? 1 : dp[l][k - 1];
int right = k == r ? 1 : dp[k + 1][r];
int score = left * right + w[k];
if(dp[l][r] < score){
dp[l][r] = score;
g[l][r] = k;
}
}
}
}
}
cout << dp[1][n] << endl;
print(1, n);
}
棋盘分割(二维区间dp)
思想:
将棋盘分割,加入当前棋盘为(x1,y1)(x2,y2),那么可以根据i从x1->x2分割为不同的横条,然后选择上面的横条还是下面的横条,也可以根据i从y1->y2分割为不同的纵条,然后选择左边的纵条合适右边的纵条,从这些方案中找最小值,直到分成了k块或者是不能够再分了为止。
注意好除法时候的整数取整。
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N = 9, M = 17;
const double INF = 1e9;
int s[N][N];
double dp[N][N][N][N][M];
double X;
int n;
double get(int x1, int y1, int x2, int y2){
double sum = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] - X;
return sum * sum / n;
}
double f(int x1, int y1, int x2, int y2, int k){
double &v = dp[x1][y1][x2][y2][k];
if(v >= 0) return v;
if(k == 1) return v = get(x1, y1, x2, y2);
v = INF;
for(int i = x1; i < x2; i ++){//横切.
v = min(f(x1, y1, i, y2, k - 1) + get(i + 1, y1, x2, y2), v);
v = min(v, f(i + 1, y1, x2, y2, k - 1) + get(x1, y1, i, y2));
}
for(int i = y1; i < y2; i ++){
v = min(f(x1, y1, x2, i, k - 1) + get(x1, i + 1, x2, y2), v);
v = min(f(x1, i + 1, x2, y2, k - 1) + get(x1, y1, x2, i), v);
}
}
int main(){
cin >> n;
for(int i = 1; i <= 8; i ++){
for(int j = 1; j <= 8; j ++){
cin >> s[i][j];
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
}
memset(dp, -1, sizeof dp);
X = (double) s[8][8] / n;
printf("%.3f\n", sqrt(f(1, 1, 8, 8, n)));
return 0;
}
树形DP
树的直径:
①无向边(任取一点作为起点,找到离这个点最长的点,然后将这个最长的点作为起点,再做一次,此时最长的点就是直径)
②有向边(找到所有点的子节点的最大路径和次大路径,然后最大值就是所有结点的最大路径和次大路径的最大值)
#include<cstring>
#include<iostream>
using namespace std;
const int N = 100010, M = 2 * N;
int h[N], e[M], ne[M], w[M], idx;
int d1[N], d2[N];
int n, maxd;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dfs_d(int u, int father){
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(j != father){
dfs_d(j, u);
if(d1[j] + w[i] > d1[u]){
d2[u] = d1[u];
d1[u] = d1[j] + w[i];
}
else if(d1[j] + w[i] > d2[u]){
d2[u] = d1[j] + w[i];
}
}
}
maxd = max(maxd, d1[u] + d2[u]);
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
for(int i = 0; i < n -1; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs_d(1, -1);
cout << maxd << endl;
}
旅游规划(树形DP)
思想:
显然这是一个简单的树形dp的问题,且是求树的直径的问题,但是本题的最长直径可能有多个,且需要将所有的在最长直径上的城市都输出,因此知道所有的结点的长度。而由于树的直径的通用解法只能够得到最长距离,对于非根结点,其最长距离和次长距离之和就不是树的直径了(因为不能往回走到父节点,只能自上而下的走),所以还需要计算每个结点往上走的路径,然后和最长次长路径之和比较,得到是否在树的直径上。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e5 + 10, M = 2 * N;
int n;
int h[N], e[M], ne[M], idx;
int d1[N], d2[N], p1[N], up[N];
int maxd;
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void dfs_d(int u, int father){
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(j != father){
dfs_d(j, u);
int distance = d1[j] + 1;
if(distance > d1[u]){
d2[u] = d1[u], d1[u] = distance;
p1[u] = j;
}
else if(distance > d2[u]) d2[u] = distance;
}
}
maxd = max(d1[u] + d2[u], maxd);
}
void dfs_up(int u, int father){
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(j != father){
up[j] = up[u] + 1;
if(p1[u] == j) up[j] = max(up[j], d2[u] + 1);
else up[j] = max(up[j], d1[u] + 1);
dfs_up(j, u);
}
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
add(b, a);
}
dfs_d(0, -1);
dfs_up(0, -1);
for(int i = 0; i < n; i ++){
int d[3] = {d1[i], d2[i], up[i]};
sort(d, d + 3);
if(d[1] + d[2] == maxd) cout << i << endl;
}
return 0;
}
树的中心(各结点的最长距离最小值)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 10010, M = N * 2, INF = 0x3f3f3f3f;
int n;
int h[N], ne[M], e[M], idx, w[M];
int d1[N], d2[N], up[N];
int p1[N], p2[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dfs_d(int u, int father){
d1[u] = d2[u] = -INF;
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(j != father){
dfs_d(j, u);
if(d1[j] + w[i] > d1[u]){
d2[u] = d1[u];
d1[u] = d1[j] + w[i];
p2[u] = p1[u];
p1[u] = j;
}
else if(d1[j] + w[i] > d2[i]){
d2[u] = d1[j] + w[i];
p2[u] = j;
}
}
}
if(d1[u] == -INF) d1[u] = d2[u] = 0;
}
void dfs_up(int u, int father){
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(j != father){
if(p1[u] == j) up[j] = max(up[u], d2[u]) + w[i];
else up[j] = max(d1[u], up[u]) + w[i];
dfs_up(j, u);
}
}
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs_d(1, -1);
dfs_up(1, -1);
int res = INF;
for(int i = 1; i <= n; i ++){
res = min(res, max(d1[i], up[i]));
}
cout << res << endl;
return 0;
}
数字转换
思想:
i从1到n找到,所有i的约数,计算约数之和,如果约束之和小于i,约数之和指向i。然后将i设置为true,这样子dfs算距离的时候一个树只需要dfs一个结点。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 50010 ;
int n;
int h[N], e[N], ne[N], idx;
int sum[N];
bool v[N];
int ans;
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
int dfs(int u){
int d1 = 0, d2 = 0;
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
int d = dfs(j) + 1;
if(d >= d1) {
d2 = d1;
d1 = d;
}
else if(d > d2){
d2 = d;
}
}
ans = max(ans, d1 + d2);
return d1;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i ++){
for(int j = 2; j <= n / i; j ++){
sum[j * i] += i;
}
}
memset(h, - 1, sizeof h);
for(int i = 2; i <= n; i ++){
if(i > sum[i]){//为什么让sum[i]指向i是为了小指向大是所有都是这样子,不存在大的指向小的的情况。如果存在某个结点被两个结点指着,那么距离至少会减少1.
//而如果用i指向sum[i],加入10指向了sum[i]=8,而8指向了sum[i]=7,同样有其他的数的约束之和为7,那就会导致本来是10->8->7->x的一个长度为4的树变成了10->8->7<-x,这样就会让长度变成了3,
add(sum[i], i);
v[i] = true;//让一个树里面只有一个结点需要dfs
}
}
for(int i = 1; i <= n; i ++){
if(!v[i]){
dfs(i);
}
}
cout << ans << endl;
return 0;
}
二叉苹果树
思路:
有依赖的背包问题的弱化版。这里是枚举所有儿子结点的最大边的情况下(保留了自己的边)的最大值。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 110, M = 2 * N;
int h[N], ne[M], e[M], w[M], idx;
int n, m;
int dp[N][N];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
void dfs(int u, int father){
for(int i = h[u]; i != -1; i = ne[i]){
if(e[i] == father) continue;
dfs(e[i], u);
for(int j = m; j >= 0; j --){
for(int k = 0; k < j; k ++){
dp[u][j] = max(dp[u][j], dp[u][j - k - 1] + dp[e[i]][k] + w[i]);
}
}
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs(1, -1);
cout << dp[1][m] << endl;
return 0;
}
战略游戏(类似于没有上司的舞会)
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1510;
int dp[N][2];
int h[N], ne[N], e[N], idx;
int p[N];
int n;
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void dfs(int u){
dp[u][1] += 1;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
dfs(j);
dp[u][1] += min(dp[j][0], dp[j][1]);
dp[u][0] += dp[j][1];
}
}
int main(){
while(cin >> n){
memset(h, -1, sizeof h);
idx = 0;
memset(dp, 0, sizeof dp);
for(int i = 0; i <= n; i ++) p[i] = i;
for(int i = 0; i <= n - 1; i ++){
int a, b;
scanf("%d:(%d)", &a, &b);
for(int i = 0; i < b; i ++){
int c;
cin >> c;
add(a, c);
p[c] = a;
}
}
int root = -1;
for(int i = 0; i < n; i ++){
if(p[i] == i){
root = i;
break;
}
}
dfs(root);
cout << min(dp[root][1], dp[root][0]) << endl;
}
return 0;
}
皇宫看守
思路:
和上题的区别在于此处是无向边。所以可以在上面的基础上用更少的,因此有差别。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1510;
int n;
int h[N], e[N], ne[N], w[N], idx;
int dp[N][3];//0表示当前点没有警卫且由父节点查看的情况,1表示当前点没有警卫且由子结点查看的情况,2表示当前点有警卫的情况。
bool v[N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void dfs(int u){
dp[u][2] += w[u];
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
dfs(j);
dp[u][0] += min(dp[j][1], dp[j][2]);//当前结点没有警卫且由父节点查看的情况则找其子结点有警卫或者没有警卫当时由子节点查看的情况(因为父节点无警卫所以查看不了)。
dp[u][2] += min(min(dp[j][0], dp[j][1]), dp[j][2]);//当前点有警卫则找其子结点中最小的情况。
}
dp[u][1] = 1e9;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
dp[u][1] = min(dp[u][1], dp[j][2] + dp[u][0] - min(dp[j][1], dp[j][2]));//当前结点没有警卫且由子节点查看的情况,需要任一子节点有警卫即可,因此为任一子节点带警卫,而其余子节点找最小值即可。而所有子节点找最小值的和为dp[u][0],所以只需要用dp[u][0]-需要带警卫的那个点的最小值,就是除了某个点需要带警卫的最小值。
}
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
for(int i = 1; i <= n; i ++){
int id, cost, cnt;
cin >> id >> cost >> cnt;
w[id] = cost;
for(int i = 0; i < cnt; i ++){
int ver;
cin >> ver;
add(id, ver);
v[ver] = true;
}
}
int root = 0;
for(int i = 1; i <= n; i ++){
if(!v[i]){
root = i;
break;
}
}
dfs(root);
cout << min(dp[root][1], dp[root][2]) << endl;
return 0;
}
数位DP(最重要的就是初始化中的dp数组)
通常是某个区间中满足什么条件的数的个数。
思考的时候应该以所有满足这种情况的数来思考,而不是就想着当前这个数的情况。
技巧1:一般转化为求1-n的中的,然后利用前缀和的思想计算得到n-m中的。
技巧2:从树的角度考虑
如:
对于左侧分支的情况通常是先预处理出来一个dp的数组,然后对这个数组中满足条件的加起来。然后将当前的x记录下来,在一下次的时候作为条件进行判断。
假设 N 在 B 进制表示下 = a n − 1 a n − 1 . . . a 0 N在B进制表示下= a_{n - 1}a_{n-1}...a_0 N在B进制表示下=an−1an−1...a0,从 a n − 1 a_{n-1} an−1开始计算,如果 a n − 1 ≠ 0 a_{n-1}≠0 an−1=0,那就就有 a n − 1 = 1 或者大于 1 的情况 a_{n-1}=1或者大于1的情况 an−1=1或者大于1的情况,当 a n − 1 = 1 a_{n-1}=1 an−1=1,则在剩下的位数里面找到K-last-1个1。然后把last++,如果 a n − 1 > 1 a_{n-1}>1 an−1>1,就在剩下的位数里面找到K-last个1。
#include<iostream>
#include<vector>
using namespace std;
const int N = 35;
int K, B;
int f[N][N];
void init(){
for(int i = 0; i <= N; i ++){
for(int j = 0; j <= i; j ++){
if(!j) f[i][j] = 1;
else f[i][j] = f[i - 1][j - 1] + f[i - 1][j];
}
}
}
int dp(int n){
if(!n) return 0;
vector<int> nums;
while(n) nums.push_back(n % B), n /= B;
int res = 0;
int last = 0;
for(int i = nums.size() - 1; i >= 0; i --){
int x = nums[i];
if(x){
res += f[i][K - last];//当前位置放0的情况。
if(x > 1){
if(K - last - 1 >= 0) res += f[i][K - last - 1];//这里放1的情况,但是由于x > 1,所以违规了,得break。
break;
}
else{
last ++;//这里放1的情况(会在下一个i时加上)
if(last > K) break;
}
}
if(!i && last == K) res ++;//右侧分支的方案
}
return res;
}
int main(){
init();
int l, r;
cin >> l >> r >> K >> B;
cout << dp(r) - dp(l - 1) << endl;
return 0;
}
数字游戏
思路:
记录前一个数位的值,如果当前数位小于之前的数位就直接break。否则就在res上加上当前多出来的部分。
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N = 35;
typedef long long LL;
LL f[N][N];
int n, m;
void init(){
for(int i = 0; i <= 9; i ++){
f[0][i] = 1;
}
for(int i = 1; i < N; i ++){
for(int j = 0; j <= 9; j ++){
for(int k = j; k <= 9; k ++){
f[i][j] += f[i - 1][k];
}
}
}
}
int dp(int n){
if(!n) return 1;
vector<int> nums;
while(n){
nums.push_back(n % 10);
n /= 10;
}
int res = 0;
int last = 0;
for(int i = nums.size() - 1; i >= 0; i --){
int x = nums[i];//他这里直接把0的放进去了,我一直想着这个0怎么处理,吐了。
for(int j = last; j < x; j ++){//加上了所有当前位数下雨x的情况。之后的情况就是在之前的情况上进行增加。
res += f[i][j];
}
if(x < last) break;
last = x;
if(!i) res ++;
}
return res;
}
int main(){
memset(f, 0, sizeof f);
init();
while(cin >> n >> m){
if(n > m) swap(n, m);
cout << dp(m) - dp(n - 1) << endl;
}
return 0;
}
Windy数
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N = 11;
int f[N][10];
int dp(int n){
if(!n) return 0;
vector<int> nums;
while(n){
nums.push_back(n % 10);
n /= 10;
}
int res = 0;
int last = -2;
for(int i = nums.size() - 1; i >= 0; i --){
int x = nums[i];
for(int j = (i == nums.size() - 1); j < x; j ++){
if(abs(j - last) >= 2) res += f[i + 1][j];
}
if(abs(x - last) >= 2) last = x;
else break;
if(!i) res ++;
}
//从低位到高位求所有包含前导0的情况。
for(int i = 1; i < nums.size(); i ++){
for(int j = 1; j <= 9; j ++){
res += f[i][j];
}
}
return res;
}
void init(){
for(int i = 0; i <= 9; i ++) f[1][i] = 1;
for(int i = 2; i < N; i ++){
for(int j = 0; j <= 9; j ++){
for(int k = 0; k <= 9; k ++){
if(abs(j - k) >= 2) f[i][j] += f[i - 1][k];
}
}
}
}
int main(){
init();
int l, r;
cin >> l >> r;
cout << dp(r) - dp(l - 1) << endl;
return 0;
}
数字游戏II
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N = 11, M = 110;
int p;
int f[N][10][M];
int mod(int x, int mod){
return (x % mod + mod) % mod;
}
void init(){
memset(f, 0, sizeof f);
for(int i = 0; i <= 9; i ++) f[1][i][i % p]++;
for(int i = 2; i < N; i ++){
for(int j = 0; j <= 9; j ++){
for(int k = 0; k < p; k ++){
for(int x = 0; x <= 9; x ++){
f[i][j][k] += f[i - 1][x][mod(k - j, p)];//第i位数取j余k的情况,等于第i-1位数取所有的情况,只要余数是k-j就行。
}
}
}
}
}
int dp(int n){
if(!n) return 1;
vector<int> nums;
while(n){
nums.push_back(n % 10);
n /= 10;
}
int res = 0;
int last = 0;
for(int i = nums.size() - 1; i >= 0; i --){
int x = nums[i];
for(int j = 0; j < x; j ++){
res += f[i + 1][j][mod(-last, p)];
}
last += x;
if(!i && last % p == 0) res ++;
}
return res;
}
int main(){
int l, r;
while(cin >> l >> r >> p){
init();
cout << dp(r) - dp(l - 1) << endl;
}
return 0;
}
不要62
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N = 11;
int f[N][N];
void init(){
for(int i = 0; i <= 9; i ++){
if(i != 4) f[1][i] = 1;
}
for(int i = 2; i < N; i ++){
for(int j = 0; j <= 9; j ++){
for(int k = 0; k <= 9; k ++){
if((j != 4 && k != 4) && j * 10 + k != 62) f[i][j] += f[i - 1][k];
}
}
}
}
int dp(int n){
if(!n) return 1;
vector<int> nums;
while(n){
nums.push_back(n % 10);
n /= 10;
}
int res = 0;
int last = 0;
for(int i = nums.size() - 1; i >= 0; i --){
int x = nums[i];
for(int j = 0; j < x; j ++){
if(j != 4 && last * 10 + j != 62) res += f[i + 1][j];
}
if(x == 4 || last * 10 + x == 62) break;
last = x;
if(!i) res ++;
}
return res;
}
int main(){
int l, r;
init();
while(cin >> l >> r, l && r){
cout << dp(r) - dp(l - 1) << endl;
}
return 0;
}
恨7不成妻
思路
这里主要的问题是要求这些数字的平方和,和这些数的个数不同,想要用上一位递推出这一位的平方和,需要保留之前的个数,和,平方和,且状态转移的关系较为复杂,分析如下:
若当前的数为j,假设上一个状态为k,那么 j k a i a a − 1 . . . a 0 jka_ia_{a-1}...a_0 jkaiaa−1...a0的平方和与 k a i a a − 1 . . . a 0 ka_ia_{a-1}...a_0 kaiaa−1...a0有什么关系呢?假设k后面可以接的分别为 A 1 , . . . A t A_1,...A_t A1,...At那么就有 k − − − − − k_{-----} k−−−−−的平方和等于可以转化为他的所有的情况的平方和的和,也就是 ( k A 1 ) 2 + ( k A 2 ) 2 + ( k A 3 ) 2 + . . . + ( k A t ) 2 (kA_1)^2+(kA_2)^2+(kA_3)^2+...+(kA_t)^2 (kA1)2+(kA2)2+(kA3)2+...+(kAt)2,利用二项式展开每一项都有如下形式 ( k A i ) 2 = ( k ⋅ 1 0 i − 1 + A i ) 2 = k ⋅ k ⋅ 1 0 i − 1 ⋅ 1 0 i − 1 + 2 k ⋅ 1 0 i − 1 A i + A i 2 (kA_i)^2=(k·10^{i-1}+A_i)^2=k·k·10^{i-1}·10^{i-1}+2k·10^{i-1}A_i+A_i^2 (kAi)2=(k⋅10i−1+Ai)2=k⋅k⋅10i−1⋅10i−1+2k⋅10i−1Ai+Ai2
因此和为 t ⋅ k ⋅ k ⋅ 1 0 i − 1 ⋅ 1 0 i − 1 + 2 k ⋅ 1 0 i − 1 ∑ A i + ∑ A i 2 t·k·k·10^{i-1}·10^{i-1}+2k·10^{i-1}\sum{}A_i+\sum{}A_i^2 t⋅k⋅k⋅10i−1⋅10i−1+2k⋅10i−1∑Ai+∑Ai2,对于j来说,所有满足条件的k的和就是j的情况。
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 20, P = 1e9 + 7;
struct F{
int s0, s1, s2;//s0是个数(1+1+...+1),s1是和(a1+a2+..+an),s2是平方和(a1^2+a2^2+...+an^2)。
}f[N][10][7][7];//第i位数为j,且总和的余数为k,各个数位的余数为l的方案数。
int power7[N], power9[N];
int mod(LL x, int y){
return (x % y + y) % y;
}
void init(){
for(int i = 0; i <= 9; i ++){
if(i == 7) continue;
auto &v = f[1][i][i % 7][i % 7];
v.s0 ++;
v.s1 += i;
v.s2 += i * i;
}
LL power = 10;
for(int i = 2; i < N; i ++, power *= 10){
for(int j = 0; j <= 9; j ++){
if(j == 7) continue;
for(int a = 0; a < 7; a ++){
for(int b = 0; b < 7; b ++){
for(int k = 0; k <= 9; k ++){
if(k == 7) continue;
auto &v1 = f[i][j][a][b], & v2 = f[i - 1][k][mod(a - j * (power % 7), 7)][mod(b - j, 7)];
v1.s0 = (v1.s0 + v2.s0) % P;
v1.s1 = (v1.s1 + j * (power % P) % P * v2.s0 + v2.s1) % P;
v1.s2 = (v1.s2 +
j * j * (power % P) % P * (power % P) % P * v2.s0 % P +
2 * j * (power % P) % P * v2.s1 % P +
v2.s2) % P;
}
}
}
}
}
power7[0] = power9[0] = 1;
for(int i = 1; i < N; i ++){
power7[i] = power7[i - 1] * 10 % 7;
power9[i] = (LL)power9[i - 1] * 10 % P;
}
}
F get(int i, int j, int a, int b){
int s0 = 0, s1 = 0, s2 = 0;
for(int x = 0; x < 7; x ++){
for(int y = 0; y < 7; y ++){
if(x != a || y != b){
s0 = (s0 + f[i][j][x][y].s0) % P;
s1 = (s1 + f[i][j][x][y].s1) % P;
s2 = (s2 + f[i][j][x][y].s2) % P;
}
}
}
return {s0, s1, s2};
}
int dp(LL n){
if(!n) return 0;
LL backup = n % P;
vector<int> nums;
while(n){
nums.push_back(n % 10);
n /= 10;
}
int res = 0;
LL last_a = 0, last_b = 0;//a是前面的数是多少,b是前面的各个数位的和是多少
for(int i = nums.size() - 1; i >= 0; i --){
int x = nums[i];
for(int j = 0; j < x; j ++){
if(j == 7) continue;
int a = mod(-last_a % 7 * power7[i + 1], 7);
int b = mod(-last_b, 7);
auto v = get(i + 1, j, a, b);//所有当前为为j,前面的数的余数不为a,各个数位的和的余数不为b的平方和,总和及个数。
res = (res +
(last_a % P) * (last_a % P) % P * power9[i + 1] % P * power9[i + 1] % P * v.s0 % P +
2 * (last_a % P) % P * power9[i + 1] % P * v.s1 % P +
v.s2) % P;
}
if(x == 7) break;
last_a = last_a * 10 + x;
last_b = last_b + x;
if(!i && last_a % P != 0 && last_b % P != 0) res = (res + backup * backup) % P;
}
return res;
}
int main(){
init();
int t;
cin >> t;
while(t --){
LL l, r;
cin >> l >> r;
cout << mod(dp(r) - dp(l - 1), P) << endl;
}
return 0;
}
单调队列优化
最大子序和
#include<iostream>
using namespace std;
const int N = 300010, INF = 1e9;
int a[N], s[N], q[N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
int tt = -1, hh = 0;
int res = - INF;
for(int i = 0; i <= n; i ++){
if(hh <= tt && i - q[hh] > m) hh ++;
res = max(res, s[i] - s[q[hh]]);
while(hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++ tt] = i;
}
int maxv = -INF;
for(int i = 1; i <= n; i ++){
maxv = max(maxv, a[i]);
if(a[i] >= 0){
cout << res << endl;
return 0;
}
}
cout << maxv << endl;
return 0;
}
旅行问题
#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e6 + 10;
int a[N], w[N], q[N];
long long s[N];
int n;
bool v[N];
int main(){
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> a[i] >> w[i];
a[i + n] = a[i];
w[i + n] = w[i];
}
//从左往右走(这里从2*n开始,是为了让i和他后面的n里面的最小值进行比较,如果后面的最小值小于了当前的s[i]则说明从这个点开始走到q[hh]一定是没有,如果最小值大于s[i]则说明在后面n个站点都可以到达,因此就标记该站点OK)
for(int i = 1; i <= n; i ++){
s[i] = s[i + n] = a[i] - w[i];
}
for(int i = 1; i <= 2 * n; i ++) s[i] += s[i - 1];
int tt = -1, hh = 0;
for(int i = 2 * n; i >= 1; i --){
if(hh <= tt && q[hh] >= i + n) hh ++;
while(hh <= tt && s[q[tt]] >= s[i]) tt --;
q[++ tt] = i;
if(i <= n && s[i - 1] - s[q[hh]] <= 0) v[i] = true;
}
//从右往左走(算左边的最大值是为了从右边的端点向左边走的。比如s[20]=12,s[15]=5,s[q[hh]]=3,那么显然,s[20]-s[q[hh]]=9>s[20]-s[15]=5,因此从右边往左走是行的通的,打上标记。)
w[0] = w[n];
for(int i = 1; i <= n; i ++) s[i] = s[i + n] = a[i] - w[i - 1];
for(int i = 1; i <= n * 2; i ++) s[i] += s[i - 1];
tt = -1, hh = 0;
for(int i = 1; i <= 2 * n; i ++){
if(hh <= tt && q[hh] < i - n) hh ++;
if(i > n && s[i] - s[q[hh]] >= 0) v[i - n] = true;
while(hh <= tt && s[q[tt]] <= s[i]) tt --;
q[++ tt] = i;
}
for(int i = 1; i <= n; i ++){
if(v[i]) cout << "TAK" << endl;
else cout << "NIE" << endl;
}
return 0;
}
烽火传递
#include<iostream>
using namespace std;
const int N = 2e5 + 10, INF = 1e9;
int a[N], dp[N], q[N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
int tt = -1, hh = 0;
for(int i = 0; i <= n; i ++){
if(hh <= tt && q[hh] < i - m) hh ++;
dp[i] = dp[q[hh]] + a[i];
while(hh <= tt && dp[q[tt]] >= dp[i]) tt --;
q[++ tt] = i;
}
int res = INF;
for(int i = n - m + 1; i <= n; i ++){
res = min(res, dp[i]);
}
cout << res << endl;
return 0;
}
绿色通道
思路:
利用二分找到应该有多少空题。当空题大于多少是,dp最后空题的那一段<=t,就说明有解,否则就是无解。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 5e4 + 10, INF = 1e9;
int n, t;
int a[N], dp[N], q[N];
bool check(int m){
memset(dp, 0, sizeof dp);
int hh = 0, tt = -1;
for(int i = 0; i <= n; i ++){
if(hh <= tt && q[hh] < i - m - 1) hh ++;
dp[i] = dp[q[hh]] + a[i];
while(hh <= tt && dp[q[tt]] >= dp[i]) tt --;
q[++ tt] = i;
}
int res = INF;
for(int i = n - m; i <= n; i ++){
res = min(res, dp[i]);
}
if(res <= t) return true;
return false;
}
int main(){
cin >> n >> t;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
int l = 0, r = n;
while (l < r){
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
cout << l << endl;
return 0;
}
修剪草坪
思路:
dp[i]表示前i个奶牛里的合法方案,那么dp[i]的状态转移方程就是前i个奶牛里的合法方案(不选该奶牛),(选该奶牛)前区间m的合法方案加上这些奶牛全选的最大值(f[j - 1] + s[i] - s[i - j])(这里是把j这头牛去除了,已到达现有选择方案和之前的合法方案合起来不会导致不合法的情况发生。)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
LL s[N];
LL dp[N];//前i个奶牛里的合法方案
int q[N];
LL g(int i){
return dp[i - 1] - s[i];
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> s[i];
s[i] += s[i - 1];
}
int hh = 0, tt = 0;//这里的tt从0开始是为了初始化g(0)=0
for(int i = 1; i <= n; i ++){
if(q[hh] < i - m) hh ++;
dp[i] = max(dp[i - 1], g(q[hh]) + s[i]);
while(hh <= tt && g(q[tt]) <= g(i)) tt --;
q[++ tt] = i;
}
cout << dp[n] << endl;
return 0;
}
理想的正方形
思路:
通过每一行把前n个中最大的放在最右边,在每一列把前n行最大的放在最后一行,对于最小值同样处理,然后对于每一个格子进行最大值-最小值并找到最小的即可。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010, INF = 1e9;
int w[N][N], q[N];
int row_max[N][N], row_min[N][N];
int n, m, t;
void get_min(int a[], int b[], int n){
int hh = 0, tt = -1;
for(int i = 1; i <= n; i ++){
if(q[hh] <= i - t) hh ++;
while(hh <= tt && a[q[tt]] >= a[i]) tt --;
q[++ tt] = i;
b[i] = a[q[hh]];
}
}
void get_max(int a[], int b[], int n){
int hh = 0, tt = -1;
for(int i = 1; i <= n; i ++){
if(q[hh] <= i - t) hh ++;
while(hh <= tt && a[q[tt]] <= a[i]) tt --;
q[++ tt] = i;
b[i] = a[q[hh]];
}
}
int main(){
cin >> n >> m >> t;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
cin >> w[i][j];
}
}
for(int i = 1; i <= n; i ++){
get_min(w[i], row_min[i], m);
get_max(w[i], row_max[i], m);
}
int a[N], b[N], c[N];
int res = INF;
for(int i = t; i <= m; i ++){
for(int j = 1; j <= n; j ++) a[j] = row_min[j][i];//把原来的行最小值转换成a的列最小值。
get_min(a, b, n);//把b变为a的行最小值,这样子b就是行列最小值的,只是这里的b是w的转置后的。
for(int j = 1; j <= n; j ++) a[j] = row_max[j][i];
get_max(a, c, n);
for(int j = t; j <= n; j ++){
res = min(res, c[j] - b[j]);
}
}
cout << res << endl;
return 0;
}
斜率优化DP
任务安排I
思路
dp[i]代表完成前i个任务的所有方案的最小值,那么就有dp[i]= min(dp[j] + sumti(sumci - sumcj) + sj*(sumcn - sumcj) ),这里把启动时间带来的后面的所有影响提前加到了当前区间内。
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 5010;
const LL INF = 1e18;
LL sc[N], st[N], dp[N];
int n, S;
int main(){
cin >> n >> S;
for(int i = 1; i <= n; i ++){
cin >> st[i] >> sc[i];
st[i] += st[i - 1];
sc[i] += sc[i - 1];
}
for(int i = 1; i <= n; i ++){
dp[i] = INF;
for(int j = 0; j <= i - 1; j ++){
dp[i] = min(dp[i], dp[j] + (LL)st[i] * (sc[i] - sc[j]) + (LL)S * (sc[n] - sc[j]));
}
}
cout << dp[n] << endl;
return 0;
}
任务安排II
在上题的基础上加大数据量
思路:
利用斜率优化,上题已知, dp[i] = dp[j] + (LL)t[i] * (c[i] - c[j]) + (LL)S * (c[n] - c[j]),经过移项可以得到
d p j = ( S + t i ) c j + d p i − t i c i − S c n dp_j=(S+t_i)c_j+dp_i-t_ic_i-Sc_n dpj=(S+ti)cj+dpi−tici−Scn,可以知道想让 d p i dp_i dpi最小,也就是让截距尽可能的小。而k是固定的,那么选择的点应该是将整个直线从负无穷朝上以移动过程中碰到的第一个点就是使得 d p i dp_i dpi最小的点。此时的j即为需要的,那么有哪些点是一定不会被需要的呢?
可以发现,当在这个多边形内部的点,就一定不会被选择,而且有斜率是单调递增的,当当前的直线斜率大于队列中hh和hh+1的斜率的时候,hh就应该被pop出去,而且由于斜率是单调递增的,之后也不可能再加进来,因此可以用队列来做,对于插进去的元素来说,如果其和tt的斜率小于等于tt和tt-1的斜率,则说明tt应该被删除,因为新组成的点会让多边形不再是凸包,因此一直删除,直到tt和i点的斜率大于tt的斜率或者是只剩下两个点的时候。这就是单调队列的维护。
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 300010;
LL dp[N], c[N], t[N], q[N];
int n, S;
int main(){
cin >> n >> S;
for(int i = 1; i <= n; i ++){
cin >> t[i] >> c[i];
t[i] += t[i - 1];
c[i] += c[i - 1];
}
int hh = 0, tt = 0;
q[0] = 0;
for(int i = 1; i <= n; i ++){
while(hh < tt && dp[q[hh + 1]] - dp[q[hh]] < (LL)(c[q[hh + 1]] - c[q[hh]]) * (S + t[i])) hh ++;
dp[i] = dp[q[hh]] - (LL)(t[i] + S) * c[q[hh]] + (LL)t[i] * c[i] + (LL) S * c[n];
while(hh < tt && (LL)(dp[q[tt]] - dp[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (LL)(c[q[tt]] - c[q[tt - 1]]) * (dp[i] - dp[q[tt]])) tt--;
q[++ tt] = i;
}
cout << dp[n] << endl;
return 0;
}
任务安排III
在第二题的基础上让t可以为负数
思路:
和之前一样,但是这里的左边的点的元素删点是不行的了,因此这里把hh删除的操作去除,然后赋值的时候用二分查找大于等于斜率的第一个数就行了。
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 3e5 + 10;
LL c[N], dp[N], t[N];
int n, S, q[N];
int main(){
cin >> n >> S;
for(int i = 1; i <= n; i ++){
cin >> t[i] >> c[i];
t[i] += t[i - 1];
c[i] += c[i - 1];
}
int hh = 0, tt = 0;
for(int i = 1; i <= n; i ++){
int l = hh, r = tt;
while(l < r){
int mid = l + r >> 1;
if(dp[q[mid + 1]] - dp[q[mid]] > (t[i] + S) * (c[q[mid + 1]] - c[q[mid]])) r = mid;
else l = mid + 1;
}
int j = q[r];
dp[i] = dp[j] - (t[i] + S) * c[j] + t[i] * c[i] + S * c[n];
while(hh < tt && (double)(dp[q[tt]] - dp[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (double)(dp[i] - dp[q[tt]]) * (c[q[tt]] - c[q[tt - 1]])) tt --;//long long的精度都不够用,越界了。
q[++ tt] = i;
}
cout << dp[n] << endl;
return 0;
}
运输小猫
思路:
先记录所有的猫咪在的山,然后用这些猫咪玩耍的时间-山的总距离,然后排序就可以ai,后面的情况就和任务安排的一模一样了。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, P = 110;
LL a[N], t[N], d[N], s[N];
int q[N];
int n, m, p;
LL dp[P][N];//i个饲养员接j个猫的最小时间。
LL get_y(int k, int j){
return dp[j - 1][k] + s[k];
}
int main(){
cin >> n >> m >> p;
for(int i = 2; i <= n; i ++){
cin >> d[i];
d[i] += d[i - 1];
}
for(int i = 1; i <= m; i ++){
int mt;
cin >> mt >> t[i];
a[i] = t[i] - d[mt];
}
sort(a + 1, a + m + 1);
for(int i = 1; i <= m; i ++) s[i] = s[i - 1] + a[i];
memset(dp, 0x3f, sizeof dp);
for(int i = 0; i <= p; i ++) dp[i][0] = 0;
for(int j = 1; j <= p; j ++){
int hh = 0, tt = 0;
for(int i = 1; i <= m; i ++){
while(hh < tt && (get_y(q[hh + 1], j) - get_y(q[hh], j)) <= (a[i]) * (q[hh + 1] - q[hh])) hh ++;
int k = q[hh];
dp[j][i] = dp[j - 1][k] - a[i] * k + s[k] + a[i] * i - s[i];
while(hh < tt && (get_y(q[tt], j) - get_y(q[tt - 1], j)) * (i - q[tt]) >= (get_y(i, j) - get_y(q[tt], j)) * (q[tt] - q[tt - 1])) tt --;
q[++ tt] = i;
}
}
cout << dp[p][m] << endl;
return 0;
}
2. 搜索
BFS
Flood Fill
从起点开始,看是否有格子能够加进来,一直加,然后对新格子重复,直到所有格子都无法加进来且对所有新格子都做过了。
池塘计数
#include<iostream>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
char a[N][N];
queue<PII> q;
int n, m;
int dx[8] = {0, 0, 1, 1, 1, -1, -1, -1}, dy[8] = {1, -1, 0, 1, -1, 0, 1, -1};
int main(){
cin >> n >> m;
for(int i = 0; i < n; i ++) cin >> a[i];
int res = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(a[i][j] == 'W'){
q.push({i, j});
a[i][j] = '.';
res ++;
}
while(q.size()){
auto t = q.front();
q.pop();
for(int k = 0; k < 8; k ++){
int x = dx[k] + t.first, y = dy[k] + t.second;
if(x >= 0 && x < n && y >= 0 && y < m && a[x][y] == 'W'){
a[x][y] = '.';
q.push({x, y});
}
}
}
}
}
cout << res << endl;
return 0;
}
城堡房间
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 55;
int res, ans;
int a[N][N];
int n, m;
bool v[N][N];
PII dir[4] ={{0, -1}, {-1, 0}, {0, 1}, {1, 0}};
void bfs(int x, int y){
queue<PII> q;
v[x][y] = true;
int cnt = 1;
q.push({x, y});
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i < 4; i ++){
if((a[t.first][t.second] >> i) & 1) continue;
int a = dir[i].first + t.first, b = dir[i].second + t.second;
if(!v[a][b]){
v[a][b] = true;
q.push({a, b});
cnt ++;
}
}
}
res = max(res, cnt);
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> a[i][j];
}
}
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(!v[i][j]){
bfs(i, j);
ans ++;
}
}
}
cout << ans << endl;
cout << res << endl;
return 0;
}
山峰和山谷
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
int a[N][N];
bool v[N][N];
int n;
int cnt_h, cnt_d;
int dx[8] = {0, 0, 1, 1, 1, -1, -1, -1}, dy[8] = {1, -1, 0, 1, -1, 0, 1, -1};
void bfs(int x, int y){
queue<PII> q;
v[x][y] = true;
q.push({x, y});
int flag = 1, flag1 = 1;
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i < 8; i ++){
int x1 = t.first + dx[i], y1 = t.second + dy[i];
if(x1 >= 0 && x1 < n && y1 >= 0 && y1 < n){
if(a[x1][y1] == a[t.first][t.second] && !v[x1][y1]){
v[x1][y1] = true;
q.push({x1, y1});
}
if(a[x1][y1] > a[t.first][t.second]) flag = 0;//山峰没了
if(a[x1][y1] < a[t.first][t.second]) flag1 = 0;
}
}
}
if(flag) cnt_h ++;
if(flag1) cnt_d ++;
}
int main(){
cin >> n;
for(int i = 0; i < n; i ++){
for(int j = 0; j < n; j ++){
cin >> a[i][j];
}
}
for(int i = 0; i < n; i ++){
for(int j = 0; j < n; j ++){
if(!v[i][j]) bfs(i, j);
}
}
cout << cnt_h << " " << cnt_d << endl;
return 0;
}
最短路模型
迷宫问题
#include<iostream>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
int a[N][N];
int dist[N][N];
PII p[N][N];
int n;
PII ans[N * N];//最长的路径为所有格子都走过也就是n^2
int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
int main(){
cin >> n;
for(int i = 0; i < n; i ++){
for(int j = 0; j < n; j ++){
cin >> a[i][j];
}
}
queue<PII> q;
q.push({0, 0});
dist[0][0] = 1;
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i < 4; i ++){
int x1 = t.first + dx[i], y1 = t.second + dy[i];
if(x1 >= 0 && x1 < n && y1 >= 0 && y1 < n){
if(!dist[x1][y1] && a[x1][y1] != 1){
dist[x1][y1] = dist[t.first][t.second] + 1;
q.push({x1, y1});
p[x1][y1] = {t.first, t.second};
}
}
}
}
int i = n - 1, j = n - 1;
int cnt = 0;
while(i != 0 || j != 0){
ans[cnt ++] = {i, j};
int temp = p[i][j].first;
j = p[i][j].second;
i = temp;
}
cout << 0 << " " << 0 << endl;
for(int i = cnt - 1; i >= 0; i --){
cout << ans[i].first << " " << ans[i].second << endl;
}
return 0;
}
武士风度的牛(走日的牛)
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int > PII;
const int N = 155;
int dist[N][N];
char a[N][N];
PII st, ed;
int dx[8] = {1, 1, 2, 2, -1, -1, -2, -2}, dy[8] = {2, -2, 1, -1, 2, -2, 1, -1};
int n, m;
int main(){
cin >> m >> n;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> a[i][j];
if(a[i][j] == 'K') st = {i, j};
if(a[i][j] == 'H') ed = {i, j};
}
}
memset(dist, -1, sizeof dist);
queue<PII> q;
q.push(st);
dist[st.first][st.second] = 0;
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i < 8; i ++){
int x1 = dx[i] + t.first, y1 = dy[i] + t.second;
if(x1 >= 0 && x1 < n && y1 >= 0 && y1 < m){
if(dist[x1][y1] == -1 && a[x1][y1] != '*'){
dist[x1][y1] = dist[t.first][t.second] + 1;
q.push({x1, y1});
}
}
}
}
cout << dist[ed.first][ed.second] << endl;
return 0;
}
抓住那头牛
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e6 + 10;//数组稍微开大一点,一倍差不多。
int dist[N];
int n, m;
int main(){
cin >> n >> m;
memset(dist, -1, sizeof dist);
dist[n] = 0;
queue<int> q;
q.push(n);
while(q.size()){
int t = q.front();
q.pop();
if(dist[t - 1] == -1){
dist[t - 1] = dist[t] + 1;
q.push(t - 1);
}
if(t > m) continue;
if(dist[t + 1] == -1){
dist[t + 1] = dist[t] + 1;
q.push(t + 1);
}
if(dist[2 * t] == -1){
dist[2 * t] = dist[t] + 1;
q.push(2 * t);
}
}
cout << dist[m] << endl;
return 0;
}
双源BFS模型(类似超级源点的思想)
矩阵距离
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
int n, m;
char g[N][N];
int dist[N][N];
int dx[] = {1, 0, 0, -1}, dy[] = {0, 1, -1, 0};
int main(){
cin >> n >> m;
queue<PII> q;
memset(dist, -1, sizeof dist);
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> g[i][j];
if(g[i][j] == '1'){
q.push({i, j});
dist[i][j] = 0;
}
}
}
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i < 4; i ++){
int a = t.first + dx[i], b = t.second + dy[i];
if(a >= 0 && a < n && b >= 0 && b < m){
if(dist[a][b] == -1){
dist[a][b] = dist[t.first][t.second] + 1;
q.push({a, b});
}
}
}
}
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cout << dist[i][j] << " ";
}
cout << endl;
}
return 0;
}
最小步数模型(以某个状态为步数)
魔板
思路:
和正常的棋盘的BFS是一样的,但是这里每一次的状态变成了整个序列的状态,这里通常用哈希来将序列作为状态来,然后每次能做什么变化作为下一个状态,比如此题将字符串作为一个状态,那么队列中的就是一个字符串,有三种变化,找到变化的规律,然后将变化后的再次放到队列中。直到找到结束状态。那么这里的dist就得用map来存,map<string, int> ,其父状态用map<string, string>来存,从父状态到子状态用map<string, char>来存,最后dist[ed]就是到最终状态的距离,然后从结束状态倒序找到起始状态,把每次的步骤存到char中,然后再正序输出即可。
#include<iostream>
#include<queue>
#include<map>
using namespace std;
string ed;
map<string, int> dist;
map<string, char> p;
map<string, string> pf;
char l[100];
int main(){
for(int i = 0; i < 8; i ++){
int a;
cin >> a;
ed += (a + '0');//结束状态的字符串。
}
queue<string> q;
string st = "12345678";//初始状态
q.push(st);//放到队列中
dist[st] = 0;//距离0。
while(q.size()){
auto t = q.front();
q.pop();
string change1, change2, change3;
for(int i = t.size() - 1; i >= 0; i --){
change1 += t[i];//生成A操作的状态
}
if(!dist.count(change1)){//如果没有,就更新距离,并放在队列中,记录到change1是什么操作,和change1是由谁来的。
q.push(change1);
dist[change1] = dist[t] + 1;
p[change1] = 'A';
pf[change1] = t;
}
//同1
change2 += t[3];
for(int i = 0; i < 3; i ++) change2 += t[i];
for(int i = 5; i < 8; i ++) change2 += t[i];
change2 += t[4];
if(!dist.count(change2)){
q.push(change2);
dist[change2] = dist[t] + 1;
p[change2] = 'B';
pf[change2] = t;
}
//同1
change3 += t[0];
change3 += t[6];
change3 += t[1];
change3 += t[3];
change3 += t[4];
change3 += t[2];
change3 += t[5];
change3 += t[7];
if(!dist.count(change3)){
q.push(change3);
dist[change3] = dist[t] + 1;
p[change3] = 'C';
pf[change3] = t;
}
//不统一写p和pf是由于可能不存在。因为可能有的change状态被访问过了。
if(dist.count(ed)) break;//如果终止状态的距离有了,就break。
}
cout << dist[ed] << endl;
int cnt = 0;
while(ed != st){
l[cnt ++] = p[ed];
ed = pf[ed];
}
for(int i = cnt - 1; i >= 0; i --){
cout << l[i];
}
return 0;
}
双端队列广搜
在往队列中插入元素的时候,把边权小的插到队头,边权大的插到队尾,就可以保证所有达到了当前点的距离是最小的,然后以当前这个点的距离去更新其他的点,并将这个点的距离确定(等价于dijkstra算法)。
#include<iostream>
#include<cstring>
#include<deque>
using namespace std;
typedef pair<int, int> PII;
const int N = 510, M = N * N;
int n, m;
char g[N][N];
int dist[N][N];
bool v[N][N];
int bfs(){
deque<PII> q;
memset(v, false, sizeof v);
memset(dist, 0x3f, sizeof dist);
char cs[5] = "\\/\\/";//(顺序是\/\/)左上右上右下左下
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1};
q.push_back({0, 0});
dist[0][0] = 0;
while(q.size()){
auto t = q.front();
q.pop_front();
int x = t.first, y = t.second;
if(x == n && y == m) return dist[x][y];
if(v[x][y]) continue;
v[x][y] = true;//取出最小的并将其确定,这个点就是已经确定后的最小的距离的点了。(完全和dijkstra算法一样)这也是为什么要双向队列的原因,否则就没有单调性了。
for(int i = 0; i < 4; i ++){
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a > n || b < 0 || b > m || v[a][b]) continue;
int ga = x + ix[i], gb = y + iy[i];
int w = (g[ga][gb] != cs[i]);//如果对于某个点来说,他左上的符号和需要的一样的话,说明边权为0,否则的话边权为1.
int d = dist[x][y] + w;
if(d <= dist[a][b]){
dist[a][b] = d;
if(w) q.push_back({a, b});//如果边权为1,在队尾插入。如果边权为0,在队头插入。
else q.push_front({a, b});
}
}
}
return -1;
}
int main()
{
int t;
cin >> t;
while(t --){
cin >> n >> m;
for(int i = 0; i < n; i ++) cin >> g[i];
if(n + m & 1) cout << "NO SOLUTION" << endl;
else{
// for(int i = 0; i < n; i ++) cin >> g[i];//输入语句放在这里会报错,因为放在这里如果输出了NOSOLUTION就会导致有一部分矩阵没读,那么下次的n和m的读入就会出问题。
cout << bfs() << endl;
}
}
return 0;
}
有时状态的步数过多,比如一个1e5的棋盘,想要从起点走到原点是会超时的,因此有了以下两种BFS的优化方式:双向广搜和A*
双向广搜
从起点和终点同时开始BFS。因为对于BFS来说,每一层的是随指数增长的,加入一共需要n层,那么对于单向搜索就是 a n a^n an次方,对于双向广搜就是 a n 2 + a n 2 a^{\frac{n}{2}}+a^{\frac{n}{2}} a2n+a2n级别的。
字串变换
#include <iostream>
#include <cstring>
#include <algorithm>
#include <map>
#include <queue>
using namespace std;
const int N = 6;
int n;
string a[N], b[N];
int extend(queue<string> &q, map<string, int> &dista, map<string, int> &distb, string a[], string b[]){
string t = q.front();
q.pop();
for(int i = 0; i < t.size(); i ++){
for(int j = 0; j < n; j ++){
if(t.substr(i, a[j].size()) == a[j]){
string state = t.substr(0, i) + b[j] + t.substr(i + a[j].size());
if(distb.count(state)) return dista[t] + distb[state] + 1;
if(dista.count(state)) continue;
dista[state] = dista[t] + 1;
q.push(state);
}
}
}
return 11;
}
int bfs(string A, string B){
queue<string> qa, qb;//起点终点
map<string, int> dista, distb;//起点终点
qa.push(A);
qb.push(B);
dista[A] = 0, distb[B] = 0;//数据里特别加了初始和原始一样的情况
if(dista.count(B)) return 0;
while(qa.size() && qb.size()){
int t;
if(qa.size() <= qb.size()) t = extend(qa, dista, distb, a, b);
else t = extend(qb, distb, dista, b, a);
if(t <= 10) return t;
}
return 11;
}
int main()
{
string A, B;
cin >> A >> B;
map<string, string> rule;
while(cin >> a[n] >> b[n]) n ++;
// for(int i = 0; i < n; i ++) cout << a[i] << endl << b[i] << endl;
int step = bfs(A, B);
if(step > 10) cout << "NO ANSWER!" << endl;
else cout << step << endl;
return 0;
}
A*
自定义一个估价函数,每次存入队列中的是估价函数+已走距离的值。
注意估价函数必须≤真实值,比如dijkstra就是估价函数始终为0的A*算法。
数列中是按照从起点到当前点的真实距离和总当前点到终点的估计距离。也就是经过当前点的从起点到终点的估计距离,并利用优先队列存储(方便取出估计的最小值)。然后迭代。当终点第一次出队时break,就是最短路,终点第几次出队就是第几短路。
八数码
#include<iostream>
#include<queue>
#include<cstring>
#include<map>
#include<algorithm>
using namespace std;
typedef pair<int, string> PIS;
const int N = 9;
int f(string state){
int distance = 0;
for(int i = 0; i < 9; i ++){
if(state[i] != 'x'){
int zhixian = abs(i + 1 - (state[i] - '0'));
distance += zhixian % 3 + zhixian / 3;
}
}
return distance;
}
string bfs(string start){
map<string, int> dist;
map<string, pair<char, string>> prev;
priority_queue<PIS, vector<PIS>, greater<PIS>> heap;
dist[start] = 0;
// char op[] = "ulrd";
heap.push({f(start), start});
string end = "12345678x";
// int dx[4] = {-3, -1, 1, 3};
int dx[] = {-1,0,1,0},dy[] = {0,1,0,-1};
char op[] = "urdl";
while(heap.size()){
auto t = heap.top();
heap.pop();
string state = t.second;
if(state == end) break;
int idx = 0;
for(int i = 0; i < 9; i ++){
if(state[i] == 'x'){
idx = i;
break;
}
}
int x = idx / 3, y = idx % 3;
string source = state;
for(int i = 0; i < 4; i ++){
// int weizhi = idx + dx[i];//千万不能这样子直接来做,因为要考虑如果是边界的话左右就不能移动了。我一直想着这样做,导致一直距离都比较短。
// if(weizhi < 0 || weizhi >= 9) continue;
// swap(state[weizhi], state[idx]);
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a > 2 || b < 0 || b > 2) continue;
swap(state[x * 3 + y], state[a * 3 + b]);
// cout << "原始值:" << source << "现在值:" << state << " 把" << idx << "改成了" << weizhi << "操作是:" << op[i] <<endl;
if(!dist.count(state)|| dist[state] > dist[source] + 1){
dist[state] = dist[source] + 1;
prev[state] = {op[i], source};
heap.push({dist[state] + f(state), state});
}
state = source;
}
}
string res;
while(end != start){
res += prev[end].first;
end = prev[end].second;
}
reverse(res.begin(), res.end());
return res;
}
int main(){
string start, seq;
char c;
while(cin >> c){
start += c;
if(c != 'x') seq += c;
}
int cnt = 0;
for(int i = 0; i < 8; i ++){
for(int j = i; j < 8; j ++){
if(seq[i] > seq[j]) cnt ++;
}
}
if(cnt % 2) cout << "unsolvable" << endl;
else cout << bfs(start) << endl;
return 0;
}
第k短路
#include<iostream>
#include<algorithm>
#include<cstring>
#include <queue>
#define x first
#define y second
using namespace std;
const int N = 1010, M = 200010;
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
int n, m, S, T, K;
int h[N], rh[M], e[M], w[M], ne[M], idx;
int dist[N];
bool v[N];
void add(int h[], int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[T] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, T});
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(v[ver]) continue;
v[ver] = true;
for(int i = rh[ver]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
}
int f(int u){
return dist[u];
}
int Astar(){
priority_queue<PIII, vector<PIII>, greater<PIII>> heap;
heap.push({dist[S], {0, S}});//第一个是起点到终点经过这个点的虚拟距离,第二个是离起点的真实距离,第三个是端点。S离
int cnt = 0;
int cishu = 0;
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.y.y, distance = t.y.x;
if(distance > 1e5) break;//设置一下距离,当距离过大,说明某个点在队列中循环了很多次了,但是仍然没有到第k短路,说明死循环了。也就是有自环了。
if(ver == T) cnt ++;
if(cnt == K) return distance;
for(int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
heap.push({distance + w[i] + dist[j], {distance + w[i], j}});
}
}
return -1;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
memset(rh, -1, sizeof rh);
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> a >> b >> c;
add(h, a, b, c), add(rh, b, a, c);
}
cin >> S >> T >> K;
if(S == T) K ++;
dijkstra();
cout << Astar() << endl;
return 0;
}
DFS
搜索与顺序
马走日
#include<iostream>
#include<cstring>
using namespace std;
const int N = 10;
int n, m;
bool v[N][N];
int res;
int dx[] = {1, 1, 2, 2, -1, -1, -2, -2}, dy[] = {2, -2, 1, -1, 2, -2, 1, -1};
void dfs(int x, int y, int cnt){
if(cnt == n * m){
res ++;
return ;
}
for(int i = 0; i < 8; i ++){
int a = x + dx[i], b = y + dy[i];
if(a >= 0 && a < n && b >= 0 && b < m){
if(!v[a][b]){
v[a][b] = true;
dfs(a, b, cnt + 1);
v[a][b] = false;
}
}
}
}
int main(){
int t;
cin >> t;
while(t --){
int x, y;
memset(v, false, sizeof v);
res = 0;
cin >> n >> m >> x >> y;
v[x][y] = true;
dfs(x, y, 1);
cout << res << endl;
}
return 0;
}
单词接龙
#include<iostream>
using namespace std;
const int N = 25;
int v[N];
string a[N];
int n;
int cnt;
void dfs(string A){
int size_ = A.size();
cnt = max(size_, cnt);
for(int i = 0; i < A.size(); i ++){
for(int j = 0; j < n; j ++){
if(v[j] < 2){
if(A.substr(A.size() - i - 1) == a[j].substr(0, i + 1)){
v[j] ++;
dfs(A + a[j].substr(i + 1));
v[j] --;
}
}
}
}
}
int main(){
cin >> n;
for(int i = 0; i <= n; i ++) cin >> a[i];
dfs(a[n]);
cout << cnt << endl;
return 0;
}
分成互质组(利用组合的思想)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 11;
int a[N], n;
int group[N][N];
int res = N;
bool v[N];
int gcd(int a, int b){
return b ? gcd(b, a % b) : a;
}
bool check(int group[], int gc, int i){
for(int j = 0; j < gc; j ++){
if(gcd(a[group[j]], a[i]) > 1) return false;
}
return true;
}
void dfs(int g, int gc, int tc, int start){//有g个组,最后一个组的数量,一共搜了tc个元素,从start开始。
if(g >= res) return;
if(tc == n) res = g;
bool flag = true;
for(int i = start; i < n; i ++){
if(!v[i] && check(group[g], gc, i)){//全是互质数
v[i] = true;
group[g][gc] = i;
dfs(g, gc + 1, tc + 1, i + 1);//g个组,数量+1,
v[i] = false;
flag = false;
}
}
if(flag) dfs(g + 1, 0, tc, 0);
}
int main(){
cin >> n;
for(int i = 0; i < n; i ++) cin >> a[i];
dfs(1, 0, 0, 0);
cout << res << endl;
return 0;
}
优化与剪枝
一:优先搜索分支数量较少的结点
二:排除等效冗余
三:可行性剪枝
四:最优性剪枝
五:记忆化搜索(DP)
小猫爬山
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20;
int n, w[N], m;
int sum[N];
int res = N;
void dfs(int u, int k){
//最优性剪枝
if(k >= res) return;
if(u == n){
res = k;
return;
}
for(int i = 0; i < k; i ++){
//可行性剪枝
if(w[u] + sum[i] <= m){
sum[i] += w[u];
dfs(u + 1, k);
sum[i] -= w[u];//恢复现场
}
}
sum[k] = w[u];
dfs(u + 1, k + 1);
sum[k] = 0;
}
int main(){
cin >> n >> m;
for(int i = 0; i < n; i ++) cin >> w[i];
//优化搜索顺序。
sort(w, w + n);
reverse(w, w + n);
dfs(0, 0);
cout << res << endl;
return 0;
}
数独
思路
优化顺序(从每个小cell中和行列中找到&后1最少的作为最先选择的)
可行性剪枝(只有&之后有大于0的才可以进行)
位运算优化(利用lowbit来进行选择填的数,利用&运算来确定有多少数可以选)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 9, M = 1 << 9;
int ones[M], map[M];
int row[N], col[N], cell[3][3];
char str[100];
void init(){
for(int i = 0; i < N; i ++) row[i] = col[i] = (1 << N) - 1;
for(int i = 0; i < 3; i ++){
for(int j = 0; j < 3; j ++){
cell[i][j] = (1 << N) - 1;
}
}
}
void draw(int x, int y, int t, bool is_set){
if(is_set) str[x * N + y] = '1' + t;
else str[x * N + y] = '.';
int v = 1 << t;
if(!is_set) v = -v;
row[x] -= v;//填上代表这里没有了,所以-v,清空代表这里有了,所以要加上1 << t表示在t这里可以选了。
col[y] -= v;
cell[x / 3][y / 3] -= v;
}
int lowbit(int x){
return x & (-x);
}
int get(int x, int y){
return (row[x] & col[y] & cell[x / 3][y / 3]);
}
bool dfs(int cnt){
if(!cnt) return true;
int minv = 10;
int x, y;
for(int i = 0; i < N; i ++){
for(int j = 0; j < N; j ++){
if(str[i * N + j] == '.'){
int state = get(i, j);
if(ones[state] < minv){
x = i, y = j;
minv = ones[state];
}
}
}
}
int state = get(x, y);
while(state){
int t = map[lowbit(state)];
draw(x, y, t, true);
if(dfs(cnt - 1)) return true;
draw(x, y, t, false);
state -= lowbit(state);
}
// for(int i = state; i; i -= lowbit(i)){
// int t = map[lowbit(i)];
// draw(x, y, t, true);
// if(dfs(cnt - 1)) return true;
// draw(x, y, t, false);
// }
return false;
}
int main(){
for(int i = 0; i < M; i ++){
int cnt = 0;
int x = i;
while(x){
if(x & 1) cnt ++;
x >>= 1;
}
ones[i] = cnt;
}
for(int i = 0; i < N; i ++) map[1 << i] = i;
while(cin >> str, str[0] != 'e'){
init();
int cnt = 0;
for(int i = 0, k = 0; i < N; i ++){
for(int j = 0; j < N; j ++, k ++){
if(str[k] != '.'){
int t = str[k] - '1';
draw(i, j, t, true);
}
else cnt ++;
}
}
dfs(cnt);
cout << str << endl;
}
return 0;
}
木棒
思路
从小到大枚举木棒的长度。
从小到大填充木棍的到木棒中
优化:
①木棍的总长度为木棒长度的整数倍(可行性)
②从大到小枚举木棍(优化顺序)
③ 排除等效冗余,剪枝(1)比如木棒中(1,2,3)和(1,3,2)一样(按照组合数枚举)、剪枝(2)如果当前木棍加到当前棒中失败了,则直接略过后面相同长度的木棍。剪枝(3)如果是木棒的第一根木棍失败了,则一定失败了。剪枝(4)如果是木棒的最后一根木棍失败了,则一定失败了
为什么第一根和最后一根有这种性质呢?
是因为在第一根和最后一根如果不成立,那么其他的情况一定是可以有和第一根和最后一根一样长的替代棍(有几个小的组成的长的木棍),但是在中间则不可以,因为存在小木棍成立的方案,但是小木棍合起来的长度可以和中间的木棍不同。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 70;
int n;
int w[N], sum, length;
bool v[N];
bool dfs(int u, int s, int start){
if(u * length == sum) return true;
if(s == length) return dfs(u + 1, 0, 0);
//剪枝3.1 按照组合方式枚举
for(int i = start; i < n; i ++){
if(v[i]) continue;
if(s + w[i] > length) continue;
v[i] = true;
if(dfs(u, s + w[i], i + 1)) return true;
v[i] = false;
//剪枝3.3
if(!s) return false;
//剪枝3.4
if(s + w[i] == length) return false;
//剪枝3.2
int j = i;
while(j < n && w[j] == w[i]) j ++;
i = j - 1;
}
return false;
}
int main(){
while(cin >> n, n != 0){
memset(v, false, sizeof v);
sum = 0;
for(int i = 0; i < n; i ++){
cin >> w[i];
sum += w[i];
}
//剪枝2 优化顺序
sort(w, w + n);
reverse(w, w + n);
length = 1;
while(true){
//剪枝1
if(sum % length == 0 && dfs(0, 0, 0)){
cout << length << endl;
break;
}
length ++;
}
}
return 0;
}
生日蛋糕
思路
依次枚举每层每个半径和高度
优化:
①自底向上搜,从大到小来枚举R,H(优化搜索顺序)
②可行性剪枝(R,H的范围)
③最优性剪枝
④将之前的面积放缩可以变成体积的形式,如果后面的面积+之前的面积大于等于了最小值就可以剪枝了,
#include<iostream>
#include <cmath>
using namespace std;
const int N = 25, INF = 1e9;
int n, m;
int minv[N], mins[N];
int R[N], H[N];
int res = INF;
void dfs(int u, int v, int s){//第几层,之前的体积,之前的面积
if(v + minv[u] > n) return;
if(s + mins[u] > res) return;
if(s + 2 * (n - v) / R[u + 1] >= res) return;
if(u == 0){
if(v == n) res = s;
return;
}
for(int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r --){
for(int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h --){
int t = 0;
if(u == m) t += r * r;
R[u] = r, H[u] = h;
dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
}
}
}
int main(){
cin >> n >> m;
for(int i = 1; i <= m; i ++){
minv[i] = minv[i - 1] + i * i * i;
mins[i] = mins[i - 1] + 2 * i * i;
}
R[m + 1] = H[m + 1] = INF;
dfs(m, 0, 0);
if(res != INF) cout << res << endl;
else cout << 0 << endl;
return 0;
}
迭代加深
也是dfs的一种优化策略,因为有时dfs的最优解可能不深,但是用dfs可能会进到某些很深的层中,导致搜索的很深,从而使时间变慢,迭代加深是一种每次将深度从小到大扩展的方法,用于限制搜索的深度。
尽管在搜索的时候可能会很深才搜到,那么就要设置先搜1层,2层知道n层,但是这前n-1次的搜索相对于第n次搜索是很小的,那么就可以忽略不计,比如完全二叉树,搜索第1层是2^1,第二层…n层是2 ^n,也就是说如果是n次搜索用迭代加深可能要搜2 ^1 + 2 ^ 2 + … + 2 ^ n,但是如果深度不止n而是m的话,那么正常的搜索就是2 ^m,而2 ^ m 是远大于2 ^ 1 + … + 2 ^ n ,所以还是有在优化。
加成序列
思路
直接枚举下一个空可以取得的所有数
优化
枚举数的时候从大到小枚举。
排除等效冗余
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int n;
int path[N];
bool dfs(int u, int depth){
if(u > depth) return false;
if(path[u - 1] == n) return true;
//剪枝等效冗余
bool v[N] = {0};
for(int i = u - 1; i >= 0; i --){
for(int j = i; j >= 0; j --){
int s = path[i] + path[j];
if(s > n || path[u - 1] >= s || v[s]) continue;
v[s] = true;
path[u] = s;
if(dfs(u + 1, depth)) return true;
}
}
return false;
}
int main(){
path[0] = 1;
while(cin >> n, n){
int depth = 1;
while(!dfs(1, depth)) depth ++;
for(int i = 0; i < depth; i ++) cout << path[i] << " ";
cout << endl;
}
return 0;
}
双向dfs
送礼物
思路(空间换时间)
这题如果用dp的01背包来做,就是O(nv)显然是太大了,因此用暴搜来做,事实上,暴搜的时间复杂度是O(2^46),就是每种物品选或者不选两种情况,共46中物品,但是显然时间也是太大了,因此采用了双向dfs,就是将物品分为前半段和后半段,分别使用dfs,此时的复杂度为O(2 ^ n/2 + 2 ^ n / 2),但是显然还没有做完,将前半段的所有情况进行排序,对于所有的后半段可以组成的数,利用二分在前半段找到能够是s[i] + s[j] < w的最大值,这样就是O(2 ^ n / 2 * (n / 2)),再加上排除等效冗余等操作,能够使复杂度在1e8以内
//濒临TLE的边缘了。。
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 50, M = 1 << (N / 2);
LL w[N], n;
LL sq[M], sb[M];
LL W;
bool v[N];
int cnt;
int cnt1;
LL res = 0;
void dfs(int l, int r, LL sum, LL q[]){
for(int i = l; i <= r; i ++){
if(sum + w[i] > W) continue;
v[i] = true;
sum += w[i];
q[cnt ++] = sum;
dfs(i + 1, r, sum, q);
v[i] = false;
sum -= w[i];
}
}
bool check(int mid, int i){
return sq[mid] + sb[i] <= W;
}
int main(){
cin >> W >> n;
for(int i = 0; i < n; i ++) cin >> w[i];
//从大到小排序
sort(w, w + n);
reverse(w, w + n);
//dfs前半段
dfs(0, n / 2 - 1, 0, sq);
sq[cnt ++] = 0;
int cnt1 = cnt;
sort(sq, sq + cnt1);
//dfs后半段
cnt = 0;
dfs(n / 2, n - 1, 0, sb);
sb[cnt ++] = 0;
//对后半段用二分来前半段找<W的最大值
for(int i = 0; i < cnt; i ++){
int l = 0, r = cnt1 - 1;
while(l < r){
int mid = l + r + 1 >> 1;
if(check(mid, i)) l = mid;
else r = mid - 1;
}
if(sq[l] + sb[i] <= W) res = max(res, sq[l] + sb[i]);
}
cout << res << endl;
}
IDA*
和A*算法一样,需要一个估价函数(必须小于等于真实值),但是这里不同的是,通常与max_depth结合,当估价函数+已经走了的深度,如果其大于max_depth,那么就可以把该处给剪枝了。
排书
思路
将所有不合理的情况作为估价函数,比如说134625,1后面应该为2,所以有一个不合理,4后面应该为5,所以有两个不合理,6后面应该没有,2后面应该为3,所以总共有4个不合理,而一次修改最多能够改变3个不合理的地方,比如abc,把b拿出来放在c后面,那么修改的就是a的后面,b的后面和c的后面,最多只能修改3处,所以估价函数为不合理的情况mod3上取整。
每一层枚举所有的起点i和所有的长度len,那么就是所有的情况,然后退出dfs的条件是如果当前的层数+估价函数>depth,就需要return false,如果估价函数为0,就说明可以return true了。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 15;
int n;
int q[N];
int w[5][N];
int f(){
int res = 0;
for(int i = 0; i < n - 1; i ++){
if(q[i + 1] != q[i] + 1) res ++;
}
return (res + 2) / 3;
}
bool dfs(int u, int depth){
if(u + f() > depth) return false;
if(f() == 0) return true;
for(int len = 1; len <= n; len ++){
for(int l = 0; l + len - 1 < n; l ++){
int r = l + len - 1;
for(int k = r + 1; k < n; k ++){
memcpy(w[u], q, sizeof q);
int y = l;
//把r+1后面到k的一段放在l后面。然后l后面到r的一段放在刚刚那一段的后面
for(int x = r + 1; x <= k; x ++, y ++) q[y] = w[u][x];
for(int x = l; x <= r; x ++, y ++) q[y] = w[u][x];
if(dfs(u + 1, depth)) return true;
//恢复现场。
memcpy(q, w[u], sizeof q);
}
}
}
return false;
}
int main(){
int t;
cin >> t;
while(t --){
cin >> n;
for(int i = 0; i < n; i ++) cin >> q[i];
int depth = 0;
while(depth < 5 && !dfs(0, depth)) depth ++;
if(depth >= 5) cout << "5 or more" << endl;
else cout << depth << endl;
}
return 0;
}
3. 图论
单源最短路的建图方式
热浪
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 2510, M = 6210;
int h[N], e[M * 2], ne[M * 2], w[M * 2], idx;
int n, m, a1, a2;
int dist[N];
bool v[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dijkstra(){
priority_queue<PII, vector<PII>, greater<PII>> heap;
memset(dist, 0x3f, sizeof dist);
dist[a1] = 0;
heap.push({dist[a1], a1});
while(heap.size()){
auto t = heap.top();
heap.pop();
int distance = t.first, ver = t.second;
if(v[ver]) continue;
v[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(){
cin >> n >> m >> a1 >> a2;
memset(h, -1, sizeof h);
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dijkstra();
cout << dist[a2] << endl;
return 0;
}
信使
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 110;
int h[N], e[4 * N], ne[4 * N], w[4 * N], idx;
int dist[N];
bool v[N];
int n, m;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void spfa(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
v[1] = true;
while(q.size()){
auto t = q.front();
q.pop();
v[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];
if(!v[j]){
q.push(j);
v[j] = true;
}
}
}
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
spfa();
int res = 0;
for(int i = 1; i <= n; i ++) res = max(dist[i], res);
if(res != 0x3f3f3f3f) cout << res << endl;
else cout << -1 << endl;
return 0;
}
香甜的黄油
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 810, M = 3000, INF = 1e9;
int n, m, q;
int h[N], e[M], ne[M], w[M], idx;
int cow[N];
int dist[N];
bool v[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void spfa(int x){
memset(dist, 0x3f, sizeof dist);
dist[x] = 0;
queue<int> heap;
heap.push(x);
while(heap.size()){
auto t = heap.front();
heap.pop();
v[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];
if(!v[j]){
heap.push(j);
v[j] = true;
}
}
}
}
}
int main(){
cin >> q >> n >> m;
for(int i = 0; i < q; i ++){
int a;
cin >> a;
cow[a] ++;
}
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int res = INF;
for(int i = 1; i <= n; i ++){
spfa(i);
int sum = 0;
for(int j = 1; j <= n; j ++){
if(cow[j] && dist[j] == 0x3f3f3f3f){//如果某个牧场有牛但是和放置黄油的牧场不连通,那么就把位置设为最大值,
sum = INF;
break;
}
sum += cow[j] * dist[j];
}
res = min(sum, res);
}
cout << res << endl;
return 0;
}
最小花费
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 2010, M = N * N;
int h[N], e[M], ne[M], idx;
double w[M];
int n, m;
double dist[N];
bool v[N];
int st, ed;
void spfa(){
memset(dist, -0x3f, sizeof dist);
dist[st] = 1;
queue<int> q;
q.push(st);
v[st] = true;
while(q.size()){
auto t = q.front();
q.pop();
v[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];
if(!v[j]){
q.push(j);
v[j] = true;
}
}
}
}
}
void add(int a, int b, double c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
while(m --){
int a, b;
double c;
cin >> a >> b >> c;
c = 1 - 0.01 * c;
cout << c << endl;
add(a, b, c), add(b, a, c);
}
cin >> st >> ed;
spfa();
printf("%.8f\n", 100 / dist[ed]);
}
最优乘车
#include<iostream>
#include<cstring>
#include <queue>
#include<sstream>
using namespace std;
const int N = 510, M = N * N;
bool g[N][N];
int n, m;
int dist[N];
int stop[N];
void bfs(){
queue<int> q;
memset(dist, 0x3f, sizeof dist);
q.push(1);
dist[1] = 0;
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i <= n; i ++){
if(1 + dist[t] < dist[i] && g[t][i]){
dist[i] = 1 + dist[t];
q.push(i);
}
}
}
}
int main(){
cin >> m >> n;
//从一行中读取先变为string后从string中cin的方法
string line;
getline(cin, line);
while(m --){
getline(cin, line);
stringstream ssin(line);
int cnt = 0, p;
while(ssin >> p) stop[cnt ++] = p;
for(int i = 0; i < cnt; i ++){
for(int j = i + 1; j < cnt; j ++){
g[stop[i]][stop[j]] = true;
}
}
}
bfs();
if(dist[n] != 0x3f3f3f3f) cout << dist[n] - 1 << endl;
else cout << "NO" << endl;
}
昂贵的聘礼
思路
构造虚拟源点,源点到各个点的距离是直接购买的价格,其余的连接两个点的边是有i物品下购买j的价格,由于等级为1到100,因此枚举等级区间,也就是每次只和等级在枚举区间的人做交易的情况下,超级源点到1的距离,然后取最小值。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int w[N][N];
int n, m;
int dist[N];
bool v[N];
int level[N];
int dijkstra(int down, int up){
memset(v, false, sizeof v);
memset(dist, 0x3f, sizeof dist);
dist[0] = 0;
for(int i = 0; i <= n; i ++){
int t = -1;
for(int j = 0; j <= n; j ++){
if(!v[j] && (t == -1 || dist[j] < dist[t])) t = j;
}
v[t] = true;
for(int j = 0; j <= n; j ++){
if(level[j] >= down && level[j] <= up){
dist[j] = min(dist[j], dist[t] + w[t][j]);
}
}
}
return dist[1];
}
int main(){
cin >> m >> n;
memset(w, 0x3f, sizeof w);
for(int i = 1; i <= n; i ++) w[i][i] = 0;
for(int i = 1; i <= n; i ++){
int price, cnt;
cin >> price >> level[i] >> cnt;
w[0][i] = min(w[0][i], price);
while(cnt --){
int id, cost;
cin >> id >> cost;
w[id][i] = min(w[id][i], cost);
}
}
int res = INF;
for(int i = level[1] - m; i <= level[1]; i ++) res = min(res, dijkstra(i, i + m));
cout << res << endl;
return 0;
}
单源最短路综合建图方式
新年好
思路
先预处理出来6个点的最短路,然后dfs所有的拜访顺序,然后找到最小值即可。
#include<queue>
#include<iostream>
#include<cstring>
using namespace std;
typedef pair<int, int > PII;
const int N = 50010, M = 200010, INF = 1e9;
int h[N], e[M], ne[M], w[M], idx;
int n, m;
int dist[6][N];
int source[6];
bool v[N];
int sum, res = INF;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void spfa(int start, int dist[]){
priority_queue<PII, vector<PII>, greater<PII>> q;
dist[start] = 0;
memset(v, false, sizeof v);
q.push({dist[start], start});
while(q.size()){
auto t = q.top();
q.pop();
int ver = t.second, distance = t.first;
if(v[ver]) continue;
v[ver] = true;
for(int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
q.push({dist[j], j});
}
}
}
}
void dfs(int u, int last){
if(u == 5){
res = min(sum, res);
return;
}
for(int i = 1; i <= 5; i ++){
if(!v[i]){
sum += dist[last][source[i]];
v[i] = true;
dfs(u + 1, i);
v[i] = false;
sum -= dist[last][source[i]];
}
}
}
int main(){
cin >> n >> m;
source[0] = 1;
for(int i = 1; i <= 5; i ++) cin >> source[i];
memset(dist, 0x3f, sizeof dist);
memset(h, -1, sizeof h);
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
for(int i = 0 ; i <= 5; i ++) spfa(source[i], dist[i]);
memset(v, false, sizeof v);
dfs(0, 0);
cout << res << endl;
return 0;
}
通信线路
思路
通过二分来做,将大于mid的花费的路看为1,小于等于mid的花费看为0,如果最短路小于等于k代表可以,那么就把花费变小,知道找到满足条件的最小值。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1010, M = 20010;
int h[N], e[M], ne[M], w[M], idx;
int n, m, k;
int dist[N], distp[N];
bool v[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
int bfs(int mid){
deque<int> q;
memset(v, false, sizeof v);
memset(dist, 0x2f, sizeof dist);
q.push_back(1);
dist[1] = 0;
while(q.size()){
auto t = q.front();
q.pop_front();
if(v[t]) continue;
v[t] = true;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i], v = w[i] > mid;
if(dist[j] > dist[t] + v){
dist[j] = dist[t] + v;
if(v) q.push_back(j);
else q.push_front(j);
}
}
}
return dist[n];
}
bool check(int mid){
if(bfs(mid) <= k) return true;
return false;
}
int main()
{
cin >> n >> m >> k;
memset(h, -1, sizeof h);
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e6 + 1;
while(l < r){
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
if(l == 1e6 + 1) cout << -1 << endl;
else cout << l << endl;
return 0;
}
道路与航线
思路
由于只有道路是双向的可能有环,且题目说了加上了航线是一定无环的,那么道路一定是一团一团的,而这一团一团内部是无负权边的,因此对于这些光靠道路组成的团,可以使用dijkstra算法,然后在加入了负权边的航线,这些航线是将这些一团团的道路连接起来的,那么我们可以在道路中进行dijkstra算法,在加入航线后用拓扑排序将这些串联起来。
①读取所有道路,然后用dfs进行道路的分块。
②加入航线,计算每块道路的入度。
③拓扑排序(计算道路内部的最短路),然后如果内部的点是和内部的点相连,就更新距离,并把这些点放在队列中,如果是外部的点(其他道路团的),就更新距离,并且让另一个道路团的入度–。如果入度为0,则把这个道路团也放到拓扑排序的队列中。
为什么这样子能够让原来dijkstra中不能有负权边变成了可以有负权边了,事实上在dijkstra中不能有负权边的最终原因是因为会导致dijkstra通过贪心确定了顺序后,但是由于某个负权边,导致又需要重新更新所有点,导致之前的贪心结果都不成立了,那么自然就不能用了。但是这里,虽然是都确定了,但是此处的负权边不在dijkstra内部,意味着当我执行这块的时候是没有负权边的,因此这里是可以用的,而当这块和另一块相连的时候,虽然有负权边,但是是先反馈负权边的,也就是先把负权边的结果计算上,然后取最小距离,这样就不会让负权边后更新导致之前的结果无效的情况发生。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 25010, M = 150010, INF = 0x3f3f3f3f;
int n, mr, mp, S;
int h[N], e[M], w[M], ne[M], idx;
int dist[N], din[N];
bool v[N];
int id[N];
int bcnt;
vector<int> block[N];
queue<int> q;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dfs(int u, int flag){
id[u] = flag;
block[flag].push_back(u);
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!id[j]){
dfs(j, flag);
}
}
}
void dijkstra(int bid){
priority_queue<PII, vector<PII>, greater<PII>> heap;
for(auto ver : block[bid]) heap.push({dist[ver], ver});
// for(int i = 0; i < block[bid].size(); i ++){
// int ver = block[bid][i];
// heap.push({dist[ver], ver});
// }
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(v[ver]) continue;
v[ver] = true;
for(int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
if(id[ver] == id[j]) heap.push({dist[j], j});
}
if(id[ver] != id[j]){
din[id[j]] --;
if(din[id[j]] == 0) q.push(id[j]);
}
}
}
}
void topsort(){
memset(dist, 0x3f, sizeof dist);
dist[S] = 0;
for(int i = 1; i <= bcnt; i ++){
if(!din[i]) q.push(i);
}
while(q.size()){
int t = q.front();
q.pop();
dijkstra(t);
}
}
int main(){
cin >> n >> mr >> mp >> S;
memset(h, -1, sizeof h);
while(mr --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
//对于所有的道路进行dfs分块,分成某两两之间没有边连通的团。
for(int i = 1; i <= n; i ++){
if(!id[i]){
dfs(i, ++ bcnt);
}
}
while(mp --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
din[id[b]] ++;
}
topsort();
for(int i = 1; i <= n; i ++){
if(dist[i] > INF / 2) cout << "NO PATH" << endl;
else cout << dist[i] << endl;
}
}
最优贸易
思路
计算在达到某个点之前的最小值和某个点之后的最大值,然后对于所有点用最大值减最小值的最大值就是最大的价格。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 100010, M = 2000010;
int n, m;
int w[N];
int hs[N], ht[N], e[M], ne[M], idx;
int dmin[N], dmax[N];
bool v[N];
void add(int h[], int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void spfa(int h[], int dist[], int type){
queue<int> q;
if(type == 0){
memset(dist, 0x3f, sizeof dmin);
dist[1] = w[1];
q.push(1);
}
else{
memset(dist, -0x3f, sizeof dmax);
dist[n] = w[n];
q.push(n);
}
while(q.size()){
auto t = q.front();
q.pop();
v[t] = false;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if((type == 0 && dist[j] > min(dist[t], w[j])) || (type == 1 && dist[j] < max(dist[t], w[j]))){
if(type == 0) dist[j] = min(dist[t], w[j]);
else dist[j] = max(dist[t], w[j]);
if(!v[j]){
q.push(j);
v[j] = true;
}
}
}
}
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> w[i];
memset(hs, -1, sizeof hs);
memset(ht, -1, sizeof ht);
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(hs, a, b), add(ht, b, a);
if(c == 2) add(hs, b, a), add(ht, a, b);
}
spfa(hs, dmin, 0);
spfa(ht, dmax, 1);
int res = 0;
for(int i = 1; i <= n; i ++){
res = max(dmax[i] - dmin[i], res);
}
cout << res << endl;
return 0;
}
单元最短路的扩展方式
最短路计数
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 100010, M = 400010, mod = 100003;
int n, m;
int h[N], e[M], ne[M], idx;
int dist[N], cnt[N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void bfs(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
cnt[1] = 1;
while(q.size()){
auto t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + 1){
dist[j] = dist[t] + 1;
cnt[j] = cnt[t];
q.push(j);
}
else if(dist[j] == dist[t] + 1){
cnt[j] = (cnt[j] + cnt[t]) % mod;
}
}
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
while(m --){
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
bfs();
for(int i = 1; i <= n; i ++){
cout << cnt[i] << endl;
}
return 0;
}
选择最佳线路
超级源点
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010, M = 21010;
int h[N], e[M], ne[M], w[M], idx;
int n, m, ed;
int dist[N];
bool v[N];
void add(int a,int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dijkstra(){
memset(v, false, sizeof v);
memset(dist, 0x3f, sizeof dist);
priority_queue<PII, vector<PII>, greater<PII>> heap;
dist[0] = 0;
heap.push({0, 0});
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(v[ver]) continue;
v[ver] = true;
for(int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if(!v[j]){
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
}
}
int main(){
while(cin >> n >> m >> ed){
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int num;
cin >> num;
for(int i = 0; i < num; i ++){
int a;
cin >> a;
add(0, a, 0);
}
dijkstra();
if(dist[ed] == 0x3f3f3f3f) cout << -1 << endl;
else cout << dist[ed] << endl;
}
return 0;
}
拯救大兵瑞恩
思路
距离设为二维的,第一位表示在哪个位置,第二位表示当前手持钥匙的状态。如果是遇到了有钥匙的格子的话,就更新钥匙状态,并且更新最短距离,由于没有钥匙的话,在遍历边的时候会continue,始终到不了那边,所以总会到达有钥匙的状态。然后再从这个状态寻找最短路。
#include<iostream>
#include<deque>
#include<cstring>
#include<set>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 11, M = N * N, E = 400, P = 1 << 10;
int n, m, p, k;
int h[M], e[E], ne[E], w[E], idx;
int g[N][N], key[M];
int dist[M][P];
bool v[M][P];
set<PII> edges;
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
void build(){
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
for(int u = 0; u < 4; u ++){
int x = i + dx[u], y = j + dy[u];
if(x <= 0 || x > n || y <= 0 || y > m) continue;
int a = g[i][j], b = g[x][y];
if(edges.count({a, b}) == 0) add(a, b, 0);
}
}
}
}
int bfs(){
memset(dist, 0x3f, sizeof dist);
dist[1][0] = 0;
deque<PII> q;
q.push_back({1, 0});
while(q.size()){
PII t = q.front();
q.pop_front();
if(v[t.x][t.y]) continue;
v[t.x][t.y] = true;
if(t.x == n * m) return dist[t.x][t.y];
if(key[t.x]){
int state = t.y | key[t.x];
if(dist[t.x][state] > dist[t.x][t.y]){
dist[t.x][state] = dist[t.x][t.y];
q.push_front({t.x, state});
}
}
for(int i = h[t.x]; ~i; i = ne[i]){
int j = e[i];
if(w[i] && !((t.y >> (w[i] - 1)) & 1)) continue;
if(dist[j][t.y] > dist[t.x][t.y] + 1){
dist[j][t.y] = dist[t.x][t.y] + 1;
q.push_back({j, t.y});
}
}
}
return -1;
}
int main(){
cin >> n >> m >> p >> k;
for(int i = 1, t = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
g[i][j] = t ++;
}
}
memset(h, -1, sizeof h);
while(k --){
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
int a = g[x1][y1], b = g[x2][y2];
edges.insert({a, b}), edges.insert({b, a});
if(c) add(a, b, c), add(b, a, c);
}
build();
int s;
cin >> s;
while(s --){
int a, b, c;
cin >> a >> b >> c;
key[g[a][b]] |= 1 << c - 1;
}
cout << bfs() << endl;
return 0;
}
观光
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1010, M = 10010;
struct Ver{
int id, type, dist;
bool operator > (const Ver &w) const{
return dist > w.dist;
}
};
int n, m, S, F;
int h[N], e[M], w[M], ne[M], idx;
int dist[N][2];
bool v[N][2];
int cnt[N][2];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dijkstra(){
memset(dist, 0x3f, sizeof dist);
memset(v, 0, sizeof v);
memset(cnt, 0, sizeof cnt);
priority_queue<Ver, vector<Ver>, greater<Ver>> heap;
dist[S][0] = 0;
cnt[S][0] = 1;
heap.push({S, 0, 0});
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.id, type = t.type, distance = t.dist, count = cnt[ver][type];
if(v[ver][type]) continue;
v[ver][type] = true;
for(int i = h[ver]; ~i; i = ne[i]){
int j = e[i];
if(distance + w[i] < dist[j][0]){
dist[j][1] = dist[j][0];
cnt[j][1] = cnt[j][0];
dist[j][0] = distance + w[i];
cnt[j][0] = count;
heap.push({j, 0, dist[j][0]});
heap.push({j, 1, dist[j][1]});
}
else if(distance + w[i] == dist[j][0]){
cnt[j][0] += count;
}
else if(distance + w[i] < dist[j][1]){
cnt[j][1] = count;
dist[j][1] = distance + w[i];
heap.push({j, 1, dist[j][1]});
}
else if(distance + w[i] == dist[j][1]){
cnt[j][1] += count;
}
}
}
}
int main(){
int t;
cin >> t;
while(t --){
memset(h, -1, sizeof h);
idx = 0;
cin >> n >> m;
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cin >> S >> F;
dijkstra();
int res = cnt[F][0];
if(dist[F][1] - 1 == dist[F][0]) res += cnt[F][1];
cout << res << endl;
}
return 0;
}
多源多汇最短路(Floyd及其扩展)
可以解决的问题有
①最短路
牛的旅行
#include<iostream>
#include<cstring>
#include<cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 160, INF = 0x3f3f3f3f;
double dist[N][N];
int n;
PDD ver[N];
double maxd[N];
char g[N][N];
double get_dist(PDD a, PDD b){
return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2));
}
int main(){
cin >> n;
for(int i = 1; i <= n; i ++) cin >> ver[i].x >> ver[i].y;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
cin >> g[i][j];
}
}
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if(i != j){
if(g[i][j] == '1') dist[i][j] = get_dist(ver[i], ver[j]);
else dist[i][j] = INF;
}
}
}
for(int k = 1; k <= n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
double max_all = 0;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if(dist[i][j] != INF) max_all = max(max_all, dist[i][j]);
}
}
double res = INF;
for(int i = 1; i <= n; i ++){
for(int j = i; j <= n; j ++){
if(dist[i][j] >= INF){
double maxvi = 0, maxvj = 0;
for(int k = 1; k <= n; k ++){
if(dist[i][k] != INF) maxvi = max(dist[i][k], maxvi);
if(dist[j][k] != INF) maxvj = max(dist[j][k], maxvj);
}
if(i != j) res = min(res, maxvj + maxvi + get_dist(ver[i], ver[j]));
}
}
}
printf("%.6f\n", max(res, max_all));
return 0;
}
②传递闭包
排序
思路
每次导进来边的时候都进行一次floyd(貌似可以用二分优化,这里是由于最多有26*25/2条边,然后floyd是n3,就是n5,所以可以过。如果把n改成100就过不了了)。然后找到可能存在的边,对这些边的情况进行判定,当有自边的时候就是矛盾,当所有点中都有边相连的时候说明排好序了。否则说明还没有。如果所有边导入后都没有,则是没有排好序,如果中间出了矛盾就可以break了,如果排好序了,就寻找最小的点(从小的点过来没有边),依次输出即可。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 26;
int n, m;
bool g[N][N], d[N][N];
bool v[N];
void floyd(){
memcpy(d, g, sizeof d);
for(int k = 0; k < n; k ++){
for(int i = 0; i < n; i ++){
for(int j = 0; j < n; j ++){
d[i][j] |= d[i][k] && d[k][j];
}
}
}
}
int check(){
for(int i = 0; i < n; i ++){
if(d[i][i]) return 2;
}
for(int i = 0; i < n; i ++){
for(int j = 0; j < i; j ++){
if(d[i][j] == 0 && d[j][i] == 0) return 0;
}
}
return 1;
}
char get_min(){
for(int i = 0; i < n; i ++){
if(!v[i]){
bool flag = true;
for(int j = 0; j < n; j ++){
if(!v[j] && d[j][i]){
flag = false;
break;
}
}
if(flag){
v[i] = true;
return 'A' + i;
}
}
}
}
int main(){
while(cin >> n >> m, n || m){
memset(g, 0, sizeof g);
int t, type = 0;
for(int i = 1; i <= m; i ++){
char str[5];
cin >> str;
int a = str[0] - 'A', b = str[2] - 'A';
if(!type){
g[a][b] = 1;
floyd();
type = check();
if(type) t = i;
}
}
if(type == 2) cout << "Inconsistency found after "<< t << " relations." << endl;
else if(type == 0) cout << "Sorted sequence cannot be determined." << endl;
else{
memset(v, false, sizeof v);
cout << "Sorted sequence determined after " << t << " relations: " ;
for(int i = 0; i < n; i ++) printf("%c", get_min());
cout << "." << endl;
}
}
return 0;
}
③找最小环
观光之旅
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, INF = 1e9;
int dist[N][N];
int n, m;
int g[N][N];
int p[N][N];
int path[N], cnt;
void get_path(int i, int j){
if(p[i][j] == 0) return;
get_path(i, p[i][j]);
path[cnt ++] = p[i][j];
get_path(p[i][j], j);
}
int main(){
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for(int i = 1; i <= m; i ++){
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
int res = INF;
memcpy(dist, g, sizeof g);
for(int k = 1; k <= n; k ++){
//这里是用来判断新组成的“三元环”是否能更小(只是用1到k-1这些边)
for(int i = 1; i < k; i ++){
for(int j = i + 1; j < k; j ++){
if((long long)dist[i][j] + g[j][k] + g[k][i] < res){//这里相当于一个动态规划,目标是找到最大点为k的三元环的最小值。由于此时最短路还没有使用到k这个点,那么现在的最短路i到j都是没有经过k及k之后的点的,因此此时的三元环在之前的基础上只需要计算i点和k点相连,k点和j点相连,j点和i点相连的和的最小值是否比之前的小即可。为什么不用dist[i][k]和dist[j][k]是因为最短路就意味着经过了多个点,加入dist[i][k] 是由i - > u - > k,那么显然这种组合是比不上g[u][k] + 别的边的,那么也就没有意义再找个边,并且使用dist[i][k]可能会形成和i j 走的重叠的路,那么就有明明无法成三元环的情况却被计算了三元环。
res = dist[i][j] + g[j][k] + g[k][i];
cnt = 0;
path[cnt ++] = k;
path[cnt ++] = i;
get_path(i, j);
path[cnt ++] = j;
}
}
}
//更新最短路同时设置来的边。
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j++){
if(dist[i][j] > dist[i][k] + dist[k][j]){
dist[i][j] = dist[i][k] + dist[k][j];
p[i][j] = k;
}
}
}
}
if(res == INF) cout << "No solution." << endl;
else{
for(int i = 0; i < cnt; i ++){
cout << path[i] << " ";
}
cout << endl;
}
return 0;
}
④恰好经过k条边的最短路(倍增)
牛站
思路
说是floyd最短路,其实感觉就是dp了,这里用dp[k,i,j]表示从i到j恰好经过k条边的最短距离,那么就有dp[a+b, i, j] = dp[a, i, k] + dp[b, k, j],这里k从1到n随便取都行,但是我有点问题的是为什么a不是从1到a+b任取。可能上述即可可以包括所有的情况,利用快速幂得到dp[k, S, E],其中每次快速幂做的乘法操作是dp[ i, j] = dp[ i, k] + dp[ k, j],相当于把边倍增了。注意初始化数组,g数组的含义是只有一条边的情况。res的初始是一条边都没有的情况。类似在幂的计算中,g是1次幂的情况,res是0次幂的情况。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <map>
using namespace std;
const int N = 210;
int k, n, m, S, E;
int g[N][N];
int res[N][N];
void mul(int c[][N], int a[][N], int b[][N]){
int temp[N][N];
memset(temp, 0x3f, sizeof temp);
for(int k = 1; k <= n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
}
}
}
memcpy(c, temp, sizeof temp);
}
void qmi(){
memset(res, 0x3f, sizeof res);
for(int i = 1; i <= n; i ++) res[i][i] = 0;
while(k){
if(k & 1) mul(res, res, g);
mul(g, g, g);
k >>= 1;
}
}
int main()
{
cin >> k >> m >> S >> E;
map<int, int> ids;
memset(g, 0x3f, sizeof g);
if(!ids.count(S)) ids[S] = ++ n;
if(!ids.count(E)) ids[E] = ++ n;
S = ids[S], E = ids[E];
while(m --){
int a, b, c;
cin >> c >> a >> b;
if(!ids.count(a)) ids[a] = ++ n;//把点映射到map中,把其在map的属于的数作为点。
if(!ids.count(b)) ids[b] = ++ n;
a = ids[a], b = ids[b];
g[a][b] = g[b][a] = min(g[a][b], c);
}
qmi();
cout << res[S][E] << endl;
return 0;
}
最小生成树
最短网络
#include<iostream>
#include<cstring>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int dist[N];
int g[N][N];
int n;
bool v[N];
int prim(){
memset(dist, 0x3f, sizeof dist);
int res = 0;
for(int i = 0; i < n; i ++){
int t = -1;
for(int j = 1; j <= n; j ++){
if(!v[j] && (t == -1 || dist[t] > dist[j])){
t = j;
}
}
if(i && dist[t] == INF) return INF;
if(i) res += dist[t];
for(int j = 1; j <= n; j ++){
dist[j] = min(dist[j], g[t][j]);
}
v[t] = true;
}
return res;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
cin >> g[i][j];
}
}
cout << prim() << endl;
return 0;
}
局域网
#include<iostream>
#include<cstring>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int dist[N];
int g[N][N];
int n, m;
bool v[N];
int prim(){
memset(dist, 0x3f, sizeof dist);
int res = 0;
for(int i = 0; i < n; i ++){
int t = -1;
for(int j = 1; j <= n; j ++){
if(!v[j] && (t == -1 || dist[t] > dist[j])){
t = j;
}
}
//只有连通的点才计算res,不连通的就直接不管了
if(i && dist[t] != INF) res += dist[t];
for(int j = 1; j <= n; j ++){
dist[j] = min(dist[j], g[t][j]);
}
v[t] = true;
}
return res;
}
int main(){
cin >> n >> m;
memset(g, 0x3f, sizeof g);
int sum = 0;
while(m --){
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = c;
sum += c;
}
cout << sum - prim() << endl;
return 0;
}
繁忙的都市
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310, M = 8010;
struct Edge{
int a, b, w;
bool operator < (const Edge e){
return w < e.w;
}
}e[M];
int n, m;
int p[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i;
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> a >> b >> c;
e[i] = {a, b, c};
}
sort(e, e + m);
int res = 0;
int minv = 0;
for(int i = 0; i < m; i ++){
int a = e[i].a, b = e[i].b, w = e[i].w;
if(find(a) != find(b)){
p[find(a)] = find(b);
res ++;
minv = w;
}
}
cout << res << " " << minv << endl;
return 0;
}
联络员
思路
读入边的时候判断是否是必选的,是必选的就直接加上,并进行合并。不是必选的先放在边的数组中,然后排序做kruskal算法就行。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310, M = 8010;
struct Edge{
int a, b, w;
bool operator < (const Edge e){
return w < e.w;
}
}e[M];
int n, m;
int p[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i;
int cnt = 0, res = 0;
for(int i = 0; i < m; i ++){
int k, a, b, w;
cin >> k >> a >> b >> w;
if(k == 1){
res += w;
if(find(a) != find(b)){
p[find(a)] = find(b);
}
}
else{
e[cnt ++] = {a, b, w};
}
}
sort(e, e + cnt);
for(int i = 0; i < cnt; i ++){
int a = e[i].a, b = e[i].b, w = e[i].w;
if(find(a) != find(b)){
p[find(a)] = find(b);
res += w;
}
}
cout << res << endl;
return 0;
}
连接格点
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
struct Edge{
int a, b, w;
bool operator < (const Edge e){
return w < e.w;
}
}e[N * N * 2];
int p[N * N];
int n, m;
int get_pos(int a, int b){
return (a - 1) * m + b;
}
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
p[get_pos(i, j)] = get_pos(i, j);
}
}
int cnt = 0;
for(int i = 1; i <= n - 1; i ++){
for(int j = 1; j <= m; j ++){
e[cnt ++] = {get_pos(i, j), get_pos(i + 1, j), 1};
}
}
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m - 1; j ++){
e[cnt ++] = {get_pos(i, j), get_pos(i, j + 1), 2};
}
}
int x1, y1, x2, y2;
while(cin >> x1 >> y1 >> x2 >> y2){
p[find(get_pos(x1, y1))] = find(get_pos(x2, y2));
}
sort(e, e + cnt);//如果加边的时候是从1开始就不用排序
int res = 0;
for(int i = 0; i < cnt; i ++){
int a = e[i].a, b = e[i].b, w = e[i].w;
if(find(a) != find(b)){
p[find(a)] = find(b);
res += w;
}
}
cout << res << endl;
return 0;
}
最小生成树的拓展应用
北极通讯设备
思路
先判断连通块和通讯机是否相同,不相同则利用kruskal算法减少连通块的数量,直到相同为止,此时的边权值就是最小的d
#include <iostream>
#include <algorithm>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 510;
struct Edge{
int a, b;
double w;
bool operator < (const Edge e){
return w < e.w;
}
}e[N * N];
int n, m;
int p[N];
PII ver[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i;
int cnt = 0;
for(int i = 1; i <= n; i ++){
int x, y;
cin >> x >> y;
for(int j = 1; j < i; j ++){
e[cnt ++] = {i, j, sqrt(pow(x - ver[j].x, 2) + pow(y - ver[j].y, 2))};
}
ver[i] = {x, y};
}
sort(e, e + cnt);
double res = 0;
int ans = 1;
for(int i = 0; i < cnt; i ++){
int a = e[i].a, b = e[i].b;
double w = e[i].w;
if(ans + m - 1 == n) break;
if(find(a) != find(b)){
ans ++;
p[find(a)] = find(b);
res = w;
}
}
// cout << sqrt(pow(100, 2) + pow(100, 2)) << endl;
printf("%.2f\n", res);
return 0;
}
新的开始
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 310;
struct Edge{
int a, b, w;
bool operator <(const Edge e){
return w < e.w;
}
}e[N * N];
int n, p[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin >> n;
p[0] = 0;
int cnt = 0;
for(int i = 1; i <= n; i ++){
int v;
cin >> v;
e[cnt ++] = {0, i, v};
p[i] = i;
}
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
int v;
cin >> v;
if(j >= i + 1) e[cnt ++] = {i, j, v};
}
}
sort(e, e + cnt);
int res = 0;
for(int i = 0; i < cnt; i ++){
int a = e[i].a, b = e[i].b, w = e[i].w;
if(find(a) != find(b)){
p[find(a)] = find(b);
res += w;
}
}
cout << res << endl;
return 0;
}
走廊泼水节
思路
定义一个cnt数组表示以当前连通块的数量,kruskal算法合并两个边的时候,看下左边连通块的数量,看下右边连通块的数量,并记录下来,然后并查集合并并且记录当前连通块的数量,然后res+(当前边的长度+1)*((左连通块数量+右连通块数量)形成的完全图的边数-左连通块形成完全图的边数-右连通块形成完全图的边数-1))
y总的是左连通块数量乘右连通块数量-1。也是新生成的完全图需要补齐的边数。
这是唯一最小生成树的做法,如果把唯一最小生成树改为最小生成树的权值不变,就把当前的长度+1变为当前长度就行。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 6010;
//忘记了整数范围了,还好没有爆int
struct Edge{
int a, b, w;
bool operator < (const Edge e){
return w < e.w;
}
}e[N];
int n;
int p[N];
int cnt[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
int t;
cin >> t;
while(t --){
cin >> n;
for(int i = 1; i <= n; i ++){
p[i] = i;
cnt[i] = 1;
}
for(int i = 0; i < n - 1; i ++){
int a, b, w;
cin >> a >> b >> w;
e[i] = {a, b, w};
}
sort(e, e + n - 1);
int res = 0;
int last = 0;
for(int i = 0; i < n - 1; i ++){
int a = e[i].a, b = e[i].b, w = e[i].w;
int cnta = cnt[find(a)], cntb = cnt[find(b)];
if(find(a) != find(b)){
cnt[find(b)] += cnt[find(a)];
p[find(a)] = find(b);
}
res += (w + 1) * (cnt[find(b)] * (cnt[find(b)] - 1) / 2 - (cnta * (cnta - 1) / 2 + cntb * (cntb - 1) / 2 + 1));
}
cout << res << endl;
}
return 0;
}
秘密的奶牛运输
思路:
求次短路有两种思想
①从非树边中加边进去替换。找到替换后最小的值。
②从树边中剔除出来,找到补充后的最小的值。(就是从权值最小的开始剔除,然后从后面补充,然后求最小生成树的值,找到剔除了一个边的最小的最小生成树的值,就是次小生成树的了)
y总解法使用了方法①,首先进行最小生成树的计算,同时对使用了的点连边,并设置这个边为树边。然后对所有点求其与其他所有点的路径中最大的边,遍历所有非树边,然后记录替换树边后最小的值,这个值就是次小生成树的值(事实上由于路径中最大的边可能和非树边的长度一样,因此还需要记录路径中的次最大边(严格小于最大边),如果一样的就选择次大边进行更新,下面的代码是用最大边的。能过vj中的题)
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 510, M = 10010;
int n, m;
struct Edge{
int a, b, w;
bool f;
bool operator< (const Edge e){
return w < e.w;
}
}edge[M];
int p[N];
int dist[N][N];
int h[N], e[N * 2], w[N * 2], ne[N * 2], idx;
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void dfs(int u, int father, int maxd, int d[]){
d[u] = maxd;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(j != father){
dfs(j, u, max(maxd, w[i]), d);
}
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b, w;
cin >> a >> b >> w;
edge[i] = {a, b, w};
}
for(int i = 1; i <= n; i ++) p[i] = i;
sort(edge, edge + m);
LL sum = 0;
for(int i = 0; i < m; i ++){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = find(edge[i].a), pb = find(edge[i].b);
if(pa != pb){
p[pa] = find(pb);
sum += w;
edge[i].f = true;
add(a, b, w), add(b, a, w);
}
}
for(int i = 1; i <= n; i ++) dfs(i, -1, 0, dist[i]);
LL res = 1e18;
for(int i = 0; i < m; i ++){
if(!edge[i].f){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
if(w > dist[a][b]){
res = min(res, sum + w - dist[a][b]);
}
}
}
cout << res << endl;
return 0;
}
负环
基本都是基于spfaO(nm):
①根据每个点入队的次数判断,如果次数大于n,则说明存在负环
②根据当前每个点的最短路中所包含的边数,如果某个点的最短路所包含的边数大于等于n,则说明存在负环。
由于时间复杂度比较高,因此通常当所有点的入队次数超过2n时,我们就认为图中有很大可能是存在负环的
虫洞
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 510, M = 5210;
int h[N], e[M], w[M], ne[M], idx;
int n, m, q;
int dist[N];
bool v[N];
int cnt[N];
bool spfa(){
memset(dist, 0, sizeof dist);
memset(v, false, sizeof v);
memset(cnt, 0, sizeof cnt);
queue<int> q;
for(int i = 1; i <= n; i ++) q.push(i);
while(q.size()){
auto t = q.front();
q.pop();
v[t] = false;
if(cnt[t] >= n) return true;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
cnt[j] ++;
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
return false;
}
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
int main(){
int t;
cin >> t;
while(t --){
cin >> n >> m >> q;
memset(h, -1, sizeof h);
idx = 0;
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
while(q --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, -c);
}
if(spfa())cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
01分数规划
形如 ∑ f i ∑ t i \frac{\sum{f_i}}{\sum{t_i}} ∑ti∑fi求最大的问题是01分数规划问题
基本做法:
先二分,然后找表达式,如果满足,则在哪个区间,不满足在另一个区间,直到退出二分为止。(浮点数二分精度一般在需要的后两位)
观光奶牛
思路:
∑ f i ∑ t i > m i d \frac{\sum{f_i}}{\sum{t_i}}>mid ∑ti∑fi>mid,其中fi为点的权值,ti为边的权值,那么就有 ∑ f i > m i d ∑ t i \sum{f_i}>mid{\sum{t_i}} ∑fi>mid∑ti即 ∑ ( f i − m i d t i ) > 0 \sum{(f_i - midt_i)}>0 ∑(fi−midti)>0,就是求当比值为mid的时候是否存在正环,如果存在正环就符合条件,可以将mid取的更大。正环就是当dist[j]<dist[t] + wf[t] - mid * w[i]的条件成立就更新距离。也就是最长路的情况。当边数大于n的时候说明有正环,就return符合条件。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1010, M = 5010;
const double eps = 1e-5;
int n, m;
int wf[N];
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool v[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
bool check(double mid){
memset(v, false, sizeof v);
memset(cnt, 0, sizeof cnt);
queue<int> q;
for(int i = 1; i <= n; i ++){
q.push(i);
v[i] = true;
}
int res = 0;
while(q.size()){
auto t = q.front();
q.pop();
v[t] = false;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] < dist[t] + wf[t] - mid * w[i]){
dist[j] = dist[t] + wf[t] - mid * w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1; i <= n; i ++) cin >> wf[i];
while(m --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
double l = 0, r = 1010;
while(r - l > eps){
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.2f\n", l);
return 0;
}
单词环
思路
本题原想法以每个字符串作为一个点,然后判断点和点之间是否能够相连,而字符串的长度是作为点的权重的,但是 这样子进行判断是否能够相连显然会超时了n^2。n=1e5。
但是y总巧妙的把点变成了首尾的两个字母,字符串长度作为边权,此时点就是字符串的开头两个字和结果两个字符,且一定是可以相连的。我认为可以这么转化的原因是只要求两个字符能够相连,那么两个字符就可以代表某个一长串的字符串,同时只有相同的两个字符能够相连,让某个点对应的下一个字串是一定确定的。而第二算法在连边的时候多加上了一个信息量就是边长,对比算法一就相当于转移的一部分信息从点中到边中。
妙的流水,受不了。换成我估计就骗50%的分就走了。
但是这样做仍然会超时,因为数据量较大,因此还需要加上一个优化(经验优化,还没有理论依据的,还有一种用栈来更新的方法,我懒得改了(还是改一下吧,后面的用这个经验很多错误的地方,给整麻了,修改的话就是导入一个stack的库,同时把queue改为stack,然后front改为top即可))就是入队总次数大于2n或者3n,那么有较大可能存在环,就直接返回true
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 700, M = 1e5 + 10;
const double eps = 1e-5;
int n;
int h[N], e[M], ne[M], w[M], idx;
double dist[N];
bool v[N];
int cnt[N];
int get(string a){
return (a[0] - 'a') * 26 + a[1] - 'a';
}
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
bool check(double mid){
memset(v, false, sizeof v);
memset(cnt, 0, sizeof cnt);
queue<int> q;
for(int i = 0; i < 676; i ++){
q.push(i);
v[i] = true;
}
int count = 0;
while(q.size()){
auto t = q.front();
q.pop();
v[t] = false;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] < dist[t] + w[i] - mid){
dist[j] = dist[t] + w[i] - mid;
cnt[j] = cnt[t] + 1;
if(count ++ > 2 * n) return true;
if(cnt[j] >= 676) return true;
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
while(cin >> n, n){
memset(h, -1, sizeof h);
idx = 0;
for(int i = 0; i < n; i ++){
string a;
cin >> a;
if(a.size() >= 2) add(get(a.substr(0, 2)), get(a.substr(a.size() - 2)), a.size());
}
if(!check(0)) cout << "No solution" << endl;
else{
double l = 0, r = 1000;
while(r - l > eps){
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.2f\n", l);
}
}
return 0;
}
差分约束
①不等式组的可行解
形如 x i ≤ x j + c k x_i≤x_j+c_k xi≤xj+ck的不等式组的一组可行解
由于求最短路的时候对于t的下一个点j,如果不想要被更新,需要dist[j]<dist[t]+ w[i], 对应上述等式即i为j的下一个点,对应的边为ck。
如果想改为求最长路问题,对于t的下一个点j,如果不想要更新,需要dist[j] > dist[t] + w[i],对应上述等式即j为i的下一个点,对于边为-ck
对于以上情况如果不会判断,可以先找到所有的限制条件,如xi≥ci,也就意味着虚拟源点和xi之间边的距离是ci,可以看到此时是虚拟源点连接xi,是从小连向大,边的权重为ci,因此在右边的向左边连一条边权为右边ci的边。
算法:将不等式变为边,然后找一个虚拟源点,使得该源点一定能够遍历所有的边,求这个源点的单源最短/长路,如果存在负/正环则说明无解。无负/正环则dist[i]即为一组可行解
②求最大值或者最小值(每个自变量的最值,也就是最终的dist数组)
求的是最小值,则应该求最长路,如果是求最大值,则应该是最短路
糖果
这里有xi≥1,所以有右边连向左边的一条权重为右边的常数项的一条边。
当X=2时,A<=B-1,所以add(A,B,1)。依次填入即可。
#include<iostream>
#include<cstring>
#include<queue>
#include<stack>
using namespace std;
const int N = 1e5 + 10, M = 3 * N;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int cnt[N];
bool v[N];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
bool spfa(){
memset(dist, -0x3f, sizeof dist);
stack<int> q;
dist[0] = 0;
q.push(0);
while(q.size()){
int t = q.top();
q.pop();
v[t] = false;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] < dist[t] + w[i]){
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n + 1) return true;
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
while(m --){
int x, a, b;
cin >> x >> a >> b;
if(x == 1) add(b, a, 0), add(a, b, 0);
else if(x == 2) add(a, b, 1);
else if(x == 3) add(b, a, 0);
else if(x == 4) add(b, a, 1);
else if(x == 5) add(a, b, 0);
}
for(int i = 1; i <= n; i ++) add(0, i, 1);
if(spfa()) cout << "-1" << endl;
else{
long long res = 0;
for(int i = 1; i <= n; i ++){
res += dist[i];
}
cout << res << endl;
}
return 0;
}
区间
思想
利用前缀和,Si表示前i个数里选多少个数。那么就有问题等价于S50001的最小值为多少。因此是最长路问题。
有不等式约束
H
(
f
)
=
{
S
i
≥
S
i
−
1
S
i
−
1
≥
S
i
−
1
S
b
−
S
a
−
1
≥
c
i
∈
[
1
,
50001
]
H(f)=\left\{\begin{matrix} S_i≥S_{i-1}\\ S_{i-1}≥S_i-1\\ S_b-S_{a-1}≥c \end{matrix}\right. i∈[1,50001]
H(f)=⎩
⎨
⎧Si≥Si−1Si−1≥Si−1Sb−Sa−1≥ci∈[1,50001]
通过最长路分析,可以知道①是从i-1向i连边权为0的边。②是从i到i-1连边权为-1的边,③是从a-1到b连边权为c的边
在分析完了后,要确定虚拟的源点一定能够走到所有的边!
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 50010, M = 3 * N;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
int cnt[N];
bool v[N];
int n;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void spfa(){
memset(dist, -0x3f, sizeof dist);
queue<int> q;
q.push(0);
dist[0] = 0;
while(q.size()){
auto t = q.front();
q.pop();
v[t] = false;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] < dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
for(int i = 1; i <= 50001; i ++){
add(i - 1, i, 0);
add(i, i - 1, -1);
}
for(int i = 0; i < n; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a - 1 + 1, b + 1, c);
}
spfa();
cout << dist[50001] << endl;
}
排队布局
思路
①首先假定所有点都≤0,那么通过spfa算法找一下有没有负环,有负环说明不存在满足要求的方案。
②在①不存在负环的基础上假定x1位置为0,计算一遍最短路,如果最短路为dist[N]为INF,则说明可以任意大,否则则说明有最大距离。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1010, M = 21010, INF = 0x3f3f3f3f;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int cnt[N];
bool v[N];
int n, m1, m2;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
bool spfa(int size){
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(v, false, sizeof v);
queue<int> q;
for(int i = 1; i <= size; i ++){
q.push(i);
dist[i] = 0;
v[i] = true;
}
while(q.size()){
auto t = q.front();
q.pop();
v[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];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
cin >> n >> m1 >> m2;
memset(h, -1, sizeof h);
for(int i = 1; i < n; i ++) add(i + 1, i, 0);
while(m1 --){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
while(m2 --){
int a, b, c;
cin >> a >> b >> c;
add(b, a, -c);
}
if(spfa(n)) cout << "-1" << endl;
else {
spfa(1);
if(dist[n] == INF) cout << -2 << endl;
else cout << dist[n] << endl;
}
return 0;
}
雇佣收银员
思路
设
x
i
x_i
xi为第i时刻选择的人,那么就有
n
u
m
[
i
]
≥
x
i
≥
0
num[i]≥x_i≥0
num[i]≥xi≥0,同时对于R[i]来说,在他前面7个时刻包括当前时刻的选择的人不能少于R[i],也就是
x
i
−
7
+
x
i
−
6
+
.
.
.
+
x
i
≥
R
i
x_{i-7}+x_{i-6}+...+x_i≥R_i
xi−7+xi−6+...+xi≥Ri,其余的就没有限制了,由于这种等式约束不满足差分约束,因此想到用前缀和来写,那么就有以下约束条件:
H
(
f
)
=
{
0
≤
S
i
−
S
i
−
1
≤
n
u
m
i
S
i
−
S
i
−
1
≥
R
i
,
i
∈
[
8
,
24
]
S
i
+
S
i
+
16
−
S
24
≥
R
i
,
i
∈
[
1
,
7
]
H(f)=\left\{\begin{matrix} 0≤S_i - S_{i-1}≤num_i\\ S_i-S_{i-1}≥R_i,i∈[8,24]\\ S_i + S_{i+16} - S_{24}≥R_i,i∈[1,7] \end{matrix}\right.
H(f)=⎩
⎨
⎧0≤Si−Si−1≤numiSi−Si−1≥Ri,i∈[8,24]Si+Si+16−S24≥Ri,i∈[1,7]
虽然(3)式有三个变量,不满足差分约束的形式,但是我们可以通过枚举S24的取值,从而将S24固定下来(c>=S24>=c就行了),就变成了差分约束的形式。从0到1000枚举,枚举到1000是因为N最大为1000,如果N无限制,就应该枚举到R*24。
#include<iostream>
#include <queue>
#include<cstring>
using namespace std;
const int N = 30, M = 100;
int h[N], e[M], ne[M], w[M], idx;
int n, m;
int r[N];
int num[N];
bool v[N];
int cnt[N], dist[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void build(int s24){
memset(h, -1, sizeof h);
idx = 0;
// idx = backup_idx;
// memcpy(h, backup_h, sizeof h);
for(int i = 1; i <= n; i ++){
add(i - 1, i, 0);
add(i, i - 1, -num[i]);
}
for(int j = 1; j <= n; j ++){
if(j <= 7) add(j + 16, j, - s24 + r[j]);
else add(j - 8, j, r[j]);
}
add(0, 24, s24), add(24, 0, -s24);
}
bool spfa(int s24){
build(s24);
memset(dist, -0x3f, sizeof dist);
memset(v, false, sizeof v);
memset(cnt, 0, sizeof cnt);
queue<int> q;
q.push(0);
dist[0] = 0;
while(q.size()){
auto t = q.front();
q.pop();
v[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];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= 25) return true;
if(!v[j]){
v[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
int t;
cin >> t;
n = 24;
while(t --){
for(int i = 1; i <= n; i ++) cin >> r[i];
cin >> m;
memset(num, 0, sizeof num);
while(m --){
int a;
cin >> a;
num[a + 1] ++;
}
int flag = 1;
for(int i = 0; i <= 1000; i ++){
if(!spfa(i)){
cout << i << endl;
flag = 0;
break;
}
}
if(flag) cout << "No Solution" << endl;
}
return 0;
}
最近公共祖先
①向上标记法O(n)
从某一个点向上走到根结点,并标记走过的路径,然后另一个点向上走,当第一次碰到标记的点的时候就是最近公共祖先。
②倍增法
首先先处理出两个数组O(nlogn)
fa[i, j]表示i这个结点向上走2^j步所能走到的结点。
depth[i]表示深度
设置哨兵,为了防止i结点跳2^j跳出了根结点,规定当跳出了根结点的j,那么就射fa[i, j] = 0,同时depth[0]=0
然后将两个点跳到同一层(depth深的跳到depth浅的)
然后两个一起跳到最近公共祖先的儿子的那一层(这是由于当往上跳的时候,如果跳的太多了也是公共祖先,但不是最近的公共祖先了),具体怎么跳就是跳2^k,这个k是使得fa[i, k]!=fa[j, k]的,但是2 ^{k+1}次就会使这两个相同的情况,然后一直做,直到k=0的时候就相等,说明此时就是到了最近公共祖先的儿子结点那一层了。(logn)
祖孙查询
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 40010, M = N * 2;
int e[M], h[N], ne[M], idx;
int depth[N];
int fa[N][16];
int n, m;
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void bfs(int root){
memset(depth, 0x3f, sizeof depth);
queue<int> q;
depth[0] = 0, depth[root] = 1;
q.push(root);
while(q.size()){
auto t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(depth[j] > depth[t] + 1){
depth[j] = depth[t] + 1;
q.push(j);
fa[j][0] = t;
for(int k = 1; k <= 15; k ++){
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
}
int lca(int a, int b){
if(depth[a] < depth[b]) swap(a, b);
for(int k = 15; k >= 0; k --){
if(depth[fa[a][k]] >= depth[b]) a = fa[a][k];
}
if(a == b) return b;
for(int k = 15; k >= 0; k --){
if(fa[a][k] != fa[b][k]){
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
int main(){
cin >> n;
int root = 0;
memset(h, -1, sizeof h);
for(int i = 0; i < n; i ++){
int a, b;
cin >> a >> b;
if(b == -1) root = a;
else add(a, b), add(b, a);
}
bfs(root);
cin >> m;
while(m --){
int a, b;
cin >> a >> b;
int p = lca(a, b);
if(p == a) cout << 1 << endl;
else if(p == b) cout << 2 << endl;
else cout << 0 << endl;
}
return 0;
}
次小生成树(倍增做法求路径中最大边和严格次大边)
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 3e5 + 10, INF = 0x3f3f3f3f;
struct Edge{
int a, b, w;
bool v;
bool operator< (const Edge e){
return w < e.w;
}
}edge[M];
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int d1[N][17], d2[N][17], depth[N];
int fa[N][17];
int p[N];
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void build(){
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
if(edge[i].v){
add(a, b, w), add(b, a, w);
}
}
}
void bfs(int root){
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[root] = 1;
queue<int> q;
q.push(root);
while(q.size()){
auto t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(depth[j] > depth[t] + 1){
depth[j] = depth[t] + 1;
q.push(j);
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF;
for(int k = 1; k <= 16; k ++){
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d1[anc][k - 1], d2[anc][k - 1], d2[j][k - 1]};
d1[j][k] = d2[j][k] = -INF;
for(int u = 0; u < 4; u ++){
int d = distance[u];
if(d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if(d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
//利用倍增算法求 a点 和 b点路径树边中边权最大的边。
LL lca(int a, int b, int w){
int distance[N * 2];
int cnt = 0;
if(depth[a] < depth[b]) swap(a, b);
for(int k = 16; k >= 0; k --){
if(depth[fa[a][k]] >= depth[b]){
distance[cnt ++] = d1[a][k];
distance[cnt ++] = d2[a][k];
a = fa[a][k];
}
}
if(a != b){
for(int k = 16; k >= 0; k --){
if(fa[a][k] != fa[b][k]){
distance[cnt ++] = d1[a][k];
distance[cnt ++] = d2[a][k];
distance[cnt ++] = d1[b][k];
distance[cnt ++] = d2[b][k];
a = fa[a][k];
b = fa[b][k];
}
}
//由于是到最近公共祖先的儿子结点,所以还得加上儿子结点的这一段。
distance[cnt ++] = d1[a][0];
distance[cnt ++] = d2[a][0];
distance[cnt ++] = d1[b][0];
distance[cnt ++] = d2[b][0];
}
int dist1 = -INF, dist2 = -INF;
//这个球最长距离的都可以用排序来做,对distance排序后,dist1是最大的,dist2从后往前找第一个不等于dist1的就是了。
for(int i = 0; i < cnt; i ++){
int d = distance[i];
if(d > dist1) dist2 = dist1, dist1 = d;
else if(d != dist1 && d > dist2) dist2 = d;
}
if(w == dist1) return w - dist2;
else return w - dist1;
// if(w > dist1) return w - dist1;
// if(w > dist2) return w - dist2;
// return INF;
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i;
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> a >> b >> c;
edge[i] = {a, b, c};
}
sort(edge, edge + m);
LL sum = 0;
//构建最小生成树。
for(int i = 0; i < m; i ++){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = find(a), pb = find(b);
if(pa != pb){
p[pa] = pb;
sum += w;
edge[i].v = true;
}
}
//建树边的图
build();
//倍增去找最大边和次大边,以及祖先和深度。
bfs(1);
LL res = 1e18;
//用非树边去替换树边。
for(int i = 0; i < m; i ++){
if(!edge[i].v){
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
}
cout << res << endl;
return 0;
}
③Tarjan算法–离线求LCA O(n + m)
基于深度优先遍历的做法。
在优先遍历的过程中将点分成三大类。没访问过的,正在访问的和访问完的。没访问过的就是还没有过去的。正在访问的代表已经过去了但是子结点还没有访问完的点,访问完的点代表子结点已经遍历过了。
算法:
①先把该结点变为正在访问的点
②然后对其子结点进行深度优先遍历。然后让其子结点的祖宗结点为自己。
③对于和自己有关的所有查询,如果其祖宗已经确定,那么就找到祖宗,由于这里的祖宗结点是遍历完了所有子结点后才赋予,判断另一个点是否已经访问完了,如果是访问完的点,那么另一个点的祖宗结点就是分支口的点,也就是最近公共祖先了。如果另一个点没有访问完,那么在访问完这个点后,另一个点相关的所有查询中,这个点就是已经访问完的,那么就会在另一个点的时候记录距离。
④把该结点标记为已访问过的点。
距离
倍增做法
#include<iostream>
#include <queue>
#include<cstring>
using namespace std;
const int N = 10010, M = 2 * N;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int n, m;
int depth[N];
int fa[N][16];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
void bfs(int root){
memset(dist, 0x3f, sizeof dist);
queue<int> q;
depth[0] = 0;
depth[root] = 1;
dist[root] = 0;
q.push(root);
while(q.size()){
auto t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
depth[j] = depth[t] + 1;
fa[j][0] = t;
q.push(j);
for(int k = 1; k <= 15; k ++){
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
}
int lca(int a, int b){
if(depth[a] < depth[b]) swap(a, b);
for(int k = 15; k >= 0; k --){
if(depth[fa[a][k]] >= depth[b]) a = fa[a][k];
}
if(b == a) return b;
for(int k = 15; k >= 0; k --){
if(fa[a][k] != fa[b][k]){
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
int main(){
cin >> n >> m;
int root = 1;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
bfs(root);
while(m --){
int a, b;
cin >> a >> b;
int p = lca(a, b);
cout << dist[a] + dist[b] - 2 * dist[p] << endl;
}
return 0;
}
Tarjan做法
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, M = 20010;
int dist[N];
int h[N], e[M], w[M], ne[M], idx;
int v[N];
bool st[N];
int p[N];
vector<PII> query[N];
int res[M];
int n, m;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
void spfa(int root){
memset(dist, 0x3f, sizeof dist);
queue<int> q;
q.push(root);
dist[root] = 0;
while(q.size()){
auto t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if(!st[j]){
st[j] = true;
q.push(j);
}
}
}
}
}
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u){
v[u] = 1;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!v[j]){
tarjan(j);
p[j] = u;
}
}
for(int i = 0; i < query[u].size(); i ++){
int y = query[u][i].first, id = query[u][i].second;
int anc = find(y);
if(v[y] == 2){
res[id] = dist[y] + dist[u] - 2 * dist[anc];
}
}
v[u] = 2;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1; i <= n; i ++) p[i] = i;
for(int i = 0; i < n - 1; i ++){
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
for(int i = 0; i < m; i ++){
int a, b;
cin >> a >> b;
query[a].push_back({b, i}), query[b].push_back({a, i});
}
int root = 1;
spfa(root);
tarjan(root);
for(int i = 0; i < m; i ++){
cout << res[i] << endl;
}
return 0;
}
闇の連鎖
思路
树上差分,每个附加边会在a和b记录+1,在最近公共祖先-2。通过前缀和得到每个边的可以被附加边替代的个数,如果个数为0,则斩掉该边后可以斩掉m个附加边中的任意一个,就是m种情况,如果个数为1,斩掉主要边后只能斩掉指定的附加边,就是一种情况,大于1的时候,都不可以,所以就是0中情况。把这些情况全部加起来,就是所有的方案数。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 2e5 + 10;
int h[N], e[M], ne[M], idx, w[M];
int fa[N][17], depth[N];
int cnt[N];
LL res;
int n, m;
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void bfs(int root){
memset(depth, 0x3f, sizeof depth);
depth[0] = 0;
depth[root] = 1;
queue<int> q;
q.push(root);
while(q.size()){
auto t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if(depth[j] > depth[t] + 1){
depth[j] = depth[t] + 1;
fa[j][0] = t;
q.push(j);
for(int k = 1; k <= 16; k ++){
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
}
int lca(int a, int b){
if(depth[a] < depth[b]) swap(a, b);
for(int k = 16; k >= 0; k --){
if(depth[fa[a][k]] >= depth[b]) a = fa[a][k];
}
if(a == b) return b;
for(int k = 16; k >= 0; k --){
if(fa[a][k] != fa[b][k]){
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
void dfs(int u, int father){
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(j != father){
dfs(j, u);
cnt[u] += cnt[j];
}
}
if(cnt[u] == 1) res += 1;
else if(cnt[u] == 0 && father != -1) res += m;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++){
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
bfs(1);
for(int i = 0; i < m; i ++){
int a, b;
cin >> a >> b;
cnt[a] ++;
cnt[b] ++;
int p = lca(a, b);
cnt[p] -= 2;
}
dfs(1, -1);
cout << res << endl;
}
有向图的强连通分量
连通分量指的是对于分量中任意两点u,v,避让可以从u走到v,且从v走到u。
强连通分量指的是极大连通分量即再加上任意的点,都不是连通分量。
强连通分量可以将有向图变为有向无环图。通过将强连通分量缩为点的形式。
tarjan算法求SCC
在dfs时引入时间戳的概念,给所有的点定义两个时间戳,一个是遍历到当前点的时间戳dfn[u],另一个是从当前点开始走能到的最小时间戳low[u]。
那么就有作为一个强连通分量的最高点,一定有dfn[u] == low[u],如果dfn[u] != low[u],那么意味着low[u]<dfn[u],如果是横叉边,则意味着非连通分量,如果是后向边,则意味着非强连通分量。
void tarjan(int u){
low[u] = dfn[u] = ++ timestamp
stk[++ top] = u, in_stk[u] = true;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]){
low[u] = min(low[u], low[j]);
}
}
if(dfn[u] == low[u]){
int y;
scc_cnt ++;
//单个的点会被trajan出去,所以不用担心把别的acc分到了自己这里。
do{
y = stk[top --];
in_stk[y] = false;
id[y] = scc_cnt;
}while(y != u);
}
}
上面求出了所有的强连通分量,想变成拓扑图还得进行缩点操作
for(int i = 1; i <= n; i ++){
for(int u = h[i]; ~u; u = ne[u]){
int j = e[u];
if(id[j] != id[i]){
add(id[i], id[j], w[u]);
}
}
}
受欢迎的牛
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e4 + 10, M = 5e4 + 10;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestemp, top, stk[N];
bool in_stk[N];
int n, m;
int acc_cnt, sum[N], id[N];
int d[N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u){
dfn[u] = low[u] = ++ timestemp;
stk[++ top] = u, in_stk[u] = true;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]){
low[u] = min(low[u], low[j]);
}
}
if(low[u] == dfn[u]){
acc_cnt ++;
int y;
do{
y = stk[top --];
in_stk[y] = false;
id[y] = acc_cnt;
sum[acc_cnt] ++;
}while(y != u);
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b;
cin >> a >> b;
add(a, b);
}
for(int i = 1; i <= n; i ++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i = 1; i <= n; i ++){
for(int u = h[i]; ~u; u = ne[u]){
int j = e[u];
if(id[i] != id[j]){
d[id[i]] ++;
}
}
}
int res = 0, idx = -1;
for(int i = 1; i <= acc_cnt; i ++){
if(d[i] == 0){
res ++;
idx = i;
}
}
if(res == 1) cout << sum[idx] << endl;
else cout << 0 << endl;
return 0;
}
学校网络
#include<iostream>
#include<cstring>
using namespace std;
const int N = 110, M = N * N;
int h[N], e[M], ne[M], idx;
int n;
int low[N], dfn[N], timestemp, top, stk[N], acc_ant;
int id[N];
bool in_stk[N];
int ind[N], outd[N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u){
low[u] = dfn[u] = ++ timestemp;
stk[++ top] = u, in_stk[u] = true;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]){
low[u] = min(low[u], low[j]);
}
}
if(low[u] == dfn[u]){
int y;
acc_ant ++;
do{
y = stk[top --];
id[y] = acc_ant;
in_stk[y] = false;
}while(y != u);
}
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
for(int i = 1; i <= n; i ++){
int b = 1;
while(cin >> b && b){
add(i, b);
}
}
for(int i = 1; i <= n; i ++){
if(!dfn[i]) tarjan(i);
}
for(int i = 1; i <= n; i ++){
for(int u = h[i]; ~u; u = ne[u]){
int j = e[u];
if(id[i] != id[j]){
ind[id[j]] ++;
outd[id[i]] ++;
}
}
}
if(acc_ant != 1){
int res = 0;
for(int i = 1; i <= acc_ant; i ++){
if(ind[i] == 0){
res ++;
}
}
cout << res << endl;
int res1 = 0;
for(int i = 1; i <= acc_ant; i ++){
if(outd[i] == 0) res1 ++;
}
cout << max(res, res1) << endl;
}
else{
cout << 1 << endl << 0 << endl;
}
return 0;
}
最大半连通子图
建图方式只需要令设一个不同的头结点数组就行了,hs,然后e和ne都不需要变,主要这里长度得翻一倍,因为要多建一个图。
算法思想
tarjan算法,缩点,然后dp即可。或者用最短路也行,这里有两层循环,但是事实上只需要考虑边的情况,也就是遍历一遍所有的边,所以复杂度只是O(m)即可。
#include<iostream>
#include<cstring>
#include<unordered_set>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 2e6 + 10;
int h[N], hs[N], e[M], ne[M], idx;
int dfn[N], low[N], timestemp, top, id[N], stk[N];
bool in_stk[N];
int n, m, mod;
int sum[N], scc_cnt;
int dp[N], g[N];
void add(int h[], int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u){
low[u] = dfn[u] = ++ timestemp ;
stk[++ top] = u, in_stk[u] = true;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[j], low[u]);
}
else if(in_stk[j]){
low[u] = min(low[j], low[u]);
}
}
if(low[u] == dfn[u]){
int y;
scc_cnt ++;
do{
y = stk[top --];
in_stk[y] = false;
id[y] = scc_cnt;
sum[scc_cnt] ++;
}while(y != u);
}
}
int main(){
cin >> n >> m >> mod;
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
for(int i = 0; i < m; i ++){
int a, b;
cin >> a >> b;
add(h, a, b);
}
for(int i = 1; i <= n; i ++){
if(!dfn[i]) tarjan(i);
}
unordered_set<LL> S;
for(int i = 1; i <= n; i ++){
for(int u = h[i]; ~u; u = ne[u]){
int j = e[u];
if(id[i] != id[j]){
LL hash = id[i] * 1000000ll + id[j];
if(S.count(hash)) continue;
add(hs, id[i], id[j]);
S.insert(hash);
}
}
}
for(int i = scc_cnt; i; i --){
if(!dp[i]){
dp[i] = sum[i];
g[i] = 1;
}
for(int u = hs[i]; ~u; u = ne[u]){
int j = e[u];
if(dp[j] < dp[i] + sum[j]){
dp[j] = dp[i] + sum[j];
g[j] = g[i];
}
else if(dp[j] == dp[i] + sum[j]){
g[j] = (g[j] + g[i]) % mod;
}
}
}
int maxv = 0, maxs = 0;
for(int i = 1; i <= scc_cnt; i ++){
if(dp[i] > maxv){
maxv = dp[i];
maxs = g[i];
}
else if(dp[i] == maxv){
maxs = (maxs + g[i]) % mod;
}
}
cout << maxv << endl << maxs << endl;
return 0;
}
银河(和差分约束中的糖果是类似的,只是换了个做法)
#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = 6e5 + 10;
int n, m;
int h[N], hs[N], e[M], w[M], ne[M], idx;
int dist[N];
int low[N], dfn[N], timestemp, top, stk[N], id[N], scc_cnt, sum[N];
bool in_stk[N];
void add(int h[], int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u){
low[u] = dfn[u] = ++ timestemp;
stk[++ top] = u, in_stk[u] = true;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]){
low[u] = min(low[u], low[j]);
}
}
if(low[u] == dfn[u]){
int y;
scc_cnt ++;
do{
y = stk[top --];
in_stk[y] = false;
id[y] = scc_cnt;
sum[scc_cnt] ++;
}while(y != u);
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> c >> a >> b;
if(c == 1) add(h, a, b, 0), add(h, b, a, 0);
else if(c == 2) add(h, a, b, 1);
else if(c == 3) add(h, b, a, 0);
else if(c == 4) add(h, b, a, 1);
else if(c == 5) add(h, a, b, 0);
}
for(int i = 1; i <= n; i ++) add(h, 0, i, 1);
tarjan(0);
bool success = true;
for(int i = 0; i <= n; i ++){
for(int u = h[i]; ~u; u = ne[u]){
int j = e[u];
if(id[i] != id[j]){
add(hs, id[i], id[j], w[u]);
}
else{
if(w[u] > 0){
success = false;
break;
}
}
}
if(!success) break;
}
if(!success) cout << -1 << endl;
else{
for(int i = scc_cnt; i; i --){
for(int u = hs[i]; ~u; u = ne[u]){
int j = e[u];
dist[j] = max(dist[j], dist[i] + w[u]);
}
}
LL res = 0;
for(int i = 1; i <= scc_cnt; i ++){
res = res + (LL)dist[i] * sum[i];
}
cout << res << endl;
}
return 0;
}
无向图的双连通分量
边双连通分量
极大的不含有桥的连通分量叫做双连通分量(桥为对于某两个连通块来说有且仅有一条边将其连接起来)
怎么判断是否为桥(此时的经过的边需要不能是过来的边的):
对于某个结点x,其某个子结点y,如果有dfn[x]<low[y],则意味着从x到y的边是桥。
如何找到所有边的双连通分量:
①去除所有桥,剩下的就是边的双连通分量
②利用dfn[x] == low[x],然后此时从栈中出来的所有元素就是边的双连通分量
冗余路径
#include<iostream>
#include<cstring>
using namespace std;
const int N = 5010, M = 2e4 + 10;
int h[N], e[M], ne[M], idx;
int low[N], dfn[N], top, timestemp, stk[N], id[N], dcc_cnt;
bool is_bridge[M];//判断是否为桥
int n, m;
int d[N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u, int from){//从哪条边过来的,而不是哪个点过来的。
low[u] = dfn[u] = ++ timestemp;
stk[++ top] = u;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){//没有访问过的话就一定不是从来的边那个地方的。
tarjan(j, i);
low[u] = min(low[u], low[j]);
if(dfn[u] < low[j]) is_bridge[i] = is_bridge[i ^ 1] = true;//这是因为在进行建边的时候是0,1 2,3 4,5这样子一对一对的
}
else if(i != (from ^ 1)){
low[u] = min(low[u], low[j]);
}
}
if(dfn[u] == low[u]){
int y;
dcc_cnt ++;
do{
y = stk[top --];
id[y] = dcc_cnt;
}while(y != u);
}
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
tarjan(1, 0);
for(int i = 1; i <= n; i ++){
for(int u = h[i]; ~u; u = ne[u]){
int j = e[u];
if(id[i] != id[j]){
d[id[i]] ++;
}
}
}
int res = 0;
for(int i = 1; i <= dcc_cnt; i ++){
if(d[i] == 1) res ++;
}
cout << (res + 1) / 2 << endl;
}
点双连通分量
极大的不包含割点的连通块(割点为对于某个连通块,去除该点以及其所有相关的边,连通块不再连通)
考虑到基本上不会用到,不进行详细学习。
二分图
关押罪犯
#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e4 + 10, M = 2e5 + 10;
int h[N], e[M], w[M], ne[M], idx;
int color[N];
int n, m;
void add(int a, int b, int c){
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
bool ranse(int u, int c, int mid){
color[u] = c;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(w[i] <= mid) continue;
if(!color[j]){
if(!ranse(j, -c, mid)) return false;
}
else{
if(color[j] == c) return false;
}
}
return true;
}
bool check(int mid){
memset(color, 0, sizeof color);
for(int i = 1; i <= n; i ++){
if(!color[i]){
if(!ranse(i, 1, mid)) return false;
}
}
return true;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b, c;
cin >> 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;
}
cout << l << endl;
return 0;
}
最大匹配
棋盘覆盖
#include<iostream>
#include<cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
PII match[N][N];
bool g[N][N], st[N][N];
bool find(int x, int y){
int dx[] = {1, 0, 0, -1}, dy[] = {0, 1, -1, 0};
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) continue;
if(st[a][b] || g[a][b]) continue;
st[a][b] = true;
PII u = match[a][b];
//如果a,b这个点没有男朋友或者她的男朋友能找到别的女朋友。
if(u.x == 0 || find(u.x, u.y)){
match[a][b] = {x, y};
return true;
}
}
return false;
}
int main(){
cin >> n >> m;
while(m --){
int x, y;
cin >> x >> y;
g[x][y] = true;
}
int res = 0;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if((i + j) % 2 && !g[i][j]){
memset(st, false, sizeof st);
if(find(i, j)) res ++;
}
}
}
cout << res << endl;
return 0;
}
欧拉路径
无向连通图:欧拉路径的充要条件为度数为奇数的点仅有2个或者0个。欧拉回路的话仅有0个。
有向连通图:欧拉路径:除起点出度=入度+1,终点入度=出度+1以外,其余点均为出度=入度。回路:各点均有出度=入度。
铲雪车
当知道是欧拉回路时,显然随便在哪点或者街道,得到的时间都是一样的,而且都是所有边的距离和/速度即可。
#include<iostream>
#include<cmath>
using namespace std;
int main(){
int x, y;
cin >> x >> y;
int x1, y1, x2, y2;
double sum = 0;
while(cin >> x1 >> y1 >> x2 >> y2){
sum += 2 * sqrt(pow(y2 - y1, 2) + pow(x2 - x1, 2));
}
printf("%d:%02d", int(sum / 20000), int(round((sum / 20000 - int(sum) / 20000) * 60)));
return 0;
}
欧拉回路
思路:
先判断是否为欧拉回路(有向图和无向图有区别)
然后再欧拉回路的基础上找到非孤立点,对其进行dfs,对它的所有没有访问过的边能到达的点进行dfs,同时标记该边,dfs完了所有以后,把这个边放在ans中。那么ans中的边即为欧拉回路走过的所有边。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010, M = 400010;
int n, m, type;
int h[N], e[M], ne[M], idx;
bool used[M];
int ans[M], cnt;
int din[N], dout[N];
void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void dfs(int u){
for(int &i = h[u]; ~i;){//单纯的删掉边是没有用的,比如第一层为m条边,第二层为m-1条边...第m层为1条边
//但是回来以后,倒数第二层还是原来的2条边的一条,它还是能够循环到另一条边,第一层还是原来最开始的边,他还是可以循环到所有的边,尽管后面是没有操作的,但是还是会循环到。
//但是如果把i改为引用的话,在每次改变的时候,外面的那一层也会改变,比如说第m层到了最后一条边,那么由于是引用,第一层的i也会到最后一条边,因此可以变为m的复杂度。
//删除的方法为h[u] = ne[i]即可
int j = e[i];
if(used[i]){
i = ne[i];
continue;
}
used[i] = true;
if(type == 1) used[i ^ 1] = true;
int t;
if(type == 1){
t = i / 2 + 1;
if(i & 1) t = - t;
}
else t = i + 1;
i = ne[i];
dfs(j);
ans[++ cnt] = t;
}
}
int main(){
cin >> type >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int a, b;
cin >> a >> b;
add(a, b);
if(type == 1) add(b, a);
din[b] ++, dout[a] ++;
}
if(type == 1){
for(int i = 1; i <= n; i ++){
if(din[i] + dout[i] & 1){
cout << "NO" << endl;
return 0;
}
}
}
else{
for(int i = 1; i <= n; i ++){
if(din[i] != dout[i]){
cout << "NO" << endl;
return 0;
}
}
}
for(int i = 1; i <= n; i ++){
if(~h[i]){
dfs(i);
break;
}
}
if(cnt < m){
cout << "NO" << endl;
return 0;
}
cout << "YES" << endl;
for(int i = cnt; i; i --){
cout << ans[i] << " ";
}
return 0;
}
骑马修栅栏
#include<iostream>
using namespace std;
const int N = 510;
int g[N][N];
int m, n = 500;
int ans[1050], cnt;
int d[N];
void dfs(int u){
for(int i = 1; i <= n; i ++){
if(g[u][i]){
g[u][i] --, g[i][u] --;
dfs(i);
}
}
ans[++ cnt] = u;
}
int main(){
cin >> m;
while(m --){
int a, b;
cin >> a >> b;
g[a][b] ++, g[b][a] ++;
d[a] ++, d[b] ++;
}
int start = 1;
while(!d[start]) start ++;
for(int i = 1; i <= n; i ++){
if(d[i] % 2){
start = i;
break;
}
}
dfs(start);
for(int i = cnt; i; i --){
cout << ans[i] << endl;
}
return 0;
}
单词游戏
#include<iostream>
#include<cstring>
using namespace std;
const int N = 30, M = 1e5 + 10;
int din[N], dout[N], p[N];
int t, m;
int get(char a){
return a - 'a' + 1;
}
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin >> t;
while(t --){
cin >> m;
memset(v, false, sizeof v);
memset(din, 0, sizeof din);
memset(dout, 0, sizeof dout);
for(int i = 1; i <= 27; i ++) p[i] = i;
for(int i = 0; i < m; i ++){
string a;
cin >> a;
din[get(a[a.size() - 1])] ++, dout[get(a[0])] ++;
p[find(get(a[0]))] = find(get(a[a.size() - 1]));
}
int res = 0, res1 = 0;
bool flag = true;
for(int i = 1; i <= 27; i ++){
if(din[i] == dout[i]){
continue;
}
else if(din[i] == dout[i] - 1){
res ++;
}
else if(din[i] == dout[i] + 1){
res1 ++;
}
else{
flag = false;
break;
}
}
if(!flag){
cout << "The door cannot be opened" << endl;
}
else{
if(res > 1 || res1 > 1){
cout << "The door cannot be opened" << endl;
}
else if(res + res1 & 1){
cout << "The door cannot be opened" << endl;
}
else{
bool flag1 = true;
for(int i = 1; i <= 27; i ++){
if(p[i] == i && din[i] + dout[i] != 0){
if(flag){
flag = false;
}
else{
cout << "The door cannot be opened" << endl;
flag1 = false;
break;
}
}
}
if(flag1) cout << "Ordering is possible" << endl;
}
}
}
return 0;
}