刘汝佳github:官方题解
算法入门经典刷题经历
第九章——动态规划
树形DP
极大独立集
1.UVA1220——树形DP,唯一性判定
// 11/17 14.03
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>
#include <unordered_map>
#include <string>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 2e2+7;
vector<int> ch[N];
// set<string> st;
unordered_map<string, int> st;
int dp[N][2], n, m, root, flag;
int h[N], e[N], ne[N], cnt;
void add(int u, int v) {
e[cnt] = v, ne[cnt] = h[u], h[u] = cnt++;
}
int f[N][N];
void dfs(int u) {
dp[u][0] = 0, dp[u][1] = 1;
f[u][0] = f[u][1] = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
dfs(vv);
dp[u][0] += max(dp[vv][0], dp[vv][1]);
if (dp[vv][0] == dp[vv][1]) f[u][0] = 1;
else if (dp[vv][0] < dp[vv][1] && f[vv][1]) f[u][0] = 1;
else if (dp[vv][0] > dp[vv][1] && f[vv][0]) f[u][0] = 1;
// dp[vv][0] == dp[vv][1] ? flag = 1: flag = 0;
dp[u][1] += dp[vv][0];
if (f[vv][0]) f[u][1] = 1;
// int ans = max(dp[vv][0], dp[vv][1]);
// if (ans > dp[u][1]) dp[u][1] += ans, flag = 1;
// else if (ans == dp[u][1]) flag = 0;
}
// if (dp[u][1] == dp[u][0]) f[u][0] = f[u][1] = 1;
}
int main() {
string rootstr, str1, str2;
while (cin >> n && n) {
st.clear(), m = 0, cnt = 0, flag = 0;
memset(h, -1, sizeof h);
cin >> rootstr;
root = ++m, st[rootstr] = root;
int x, y;
for (int i = 1; i <= n-1; i++) {
cin >> str1 >> str2;
if (!st.count(str1)) {x=++m, st[str1] = x;}
else x = st[str1];
if (!st.count(str2)) {y=++m, st[str2] = y;}
else y = st[str2];
add(y, x);
}
dfs(root);
if (n == 1) {
puts("1 Yes");
continue ;
}
printf("%d ", max(dp[root][0], dp[root][1]));
flag = (dp[root][0]>dp[root][1] && f[root][0]) ||
(dp[root][1]>dp[root][0] && f[root][1]) ||
(dp[root][1] == dp[root][0]);
if (!flag) puts("Yes");
else puts("No");
}
return 0;
}
问题1:为什么第39行的会被注释掉?因为单点值相等不代表一定会两个全部会被用到,而是在转移时取 max 发生的。
问题2:为什么会用 f 数组存储?因为在记录转移的过程是否用到发生多解的状态,否则不知道多解在那发生,之后又用在哪里。
树的最长路径(换根)
1.HDU2196网络——二次换根、树的最长路径
注意:不超过109 不一定是在 int 范围内,记得开 long long !
#include <iostream> // 18.17
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 2e4+7;
int n, m;
ll h[N], e[N], ne[N], w[N], dp[N], d1[N], d2[N], p[N], up[N], cnt;
void init() {memset(h, -1, sizeof h); cnt = 0;}
void add(int u, int v, int w_) {e[cnt] = v, w[cnt] = w_, ne[cnt] = h[u], h[u] = cnt++;}
ll dfs_d(int u, int pa) {
d1[u] = d2[u] = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (vv == pa) continue ;
ll s = dfs_d(vv, u) + w[i];
if (s >= d1[u]) d2[u] = d1[u], p[u] = vv, d1[u] = s;
else if (s >= d2[u]) d2[u] = s;
}
return d1[u];
}
void dfs_u(int u, int pa) {
if(pa == 0) up[u] = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int vv = e[i];
if (vv == pa) continue;
// up[vv] = max(up[vv], up[u] + w[i]);
if (p[u] == vv) up[vv] = max(up[u], d2[u]) + w[i];
else up[vv] = max(up[u], d1[u]) + w[i];
dfs_u(vv, u);
}
}
int main() {
while (scanf("%d", &n) == 1) {
init();
for (int i = 2; i <= n; i++) {
int y, w_;
scanf("%d%d", &y, &w_);
add(i, y, w_); add(y, i, w_);
}
dfs_d(1, 0);
dfs_u(1, 0);
for (int i = 1; i <= n; i++) {
printf("%lld\n", max(d1[i], up[i]));
}
}
return 0;
}
第七章——暴力枚举
深搜(全排列,枚举二叉树等)
全排列应用
1.UVA140——全排列
解题思路:没啥思路,暴力模拟全排序即可,没什么技术含量。重点在于编码细节的处理:
(1)如果设计自己的输入函数,注意本题目是多组输入,代码注释部分的 readin() 为单组输入,未注释部分是多组输入。
(2)可使用 int 数组代替 char 数组,输出答案时直接使用 %c 格式转换输出 ASCII 码对应的字符即可。
(3)剪不剪枝无所谓。
(4)时间复杂度推算,因为数据量只有 8 个,且最多只有 8 个字母,也就是正常深搜的量级是 8!,但是因为每次填入字母的时候,需要看和之前填入的字母的距离,所以具体算式:
8*7*6*5*4*3*2*1
+(8*7)*1+(8*7*6)*2+(8*7*6*5)*3+(8*7*6*5*4)*4+…+(8!)*7。
n!+(k-1)Ckn。k 从 1到n,正好小项可视为不影响复杂度,所以最终复杂度为 n*n!。
类似的,可以证明出,只要深搜中有线性循环(不是下一层递归在函数循环里,递归和循环是并列关系,非嵌套关系),我们直接在最后的n!乘线性循环的复杂度n,可以理解为,递归的每个结点都是线性复杂度,所以最终复杂度为,结点数量*结点的复杂度,即 n*n!,平方,立方复杂度同理。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e3 + 7;
int buff[N], buff_n, n, MIN = 10;
int mp[30+'A'][30+'A'], vis[30+'A'], a[30];
int ts[30], num[30];
// bool readin() {
// char c;
// buff_n = 0;
// while(1) {
// c = getchar();
// if (c == ';' || c == '#') break;
// if (c == '\n'|| c == '\r') break;
// buff[buff_n++] = c;
// if (c != ':' && !vis[c]) vis[c] = 1, a[n++] = c;
// // printf("%c %d ", c, c);
// }
// // puts("");
// return buff_n != 0;
// }
void createMp() {
int ch = buff[0];
for (int i = 2; i < buff_n; i++) {
mp[ch][buff[i]] = 1;
mp[buff[i]][ch] = 1;
}
}
bool readin() {
char c;
buff_n = 0, n = 0;
while(1) {
c = getchar();
if (c == '#') return 0;
if (c == ';') {
createMp();
buff_n = 0;
continue;
}
if (c == '\n'|| c == '\r') {
createMp();
break;
}
buff[buff_n++] = c;
if (c != ':' && !vis[c]) vis[c] = 1, a[n++] = c;
// printf("%c %d ", c, c);
}
// puts("");
return 1;
}
void dfs(int d, int ans) {
if (d == n) {
if (ans < MIN) {
for (int i = 0; i < n; i++) num[i] = ts[i];
MIN = ans;
}
// for (int i = 0; i < n; i++) printf("%d ", ts[i]);
// puts("");
return ;
}
int t = n, ans1 = ans;
for (int i = 0; i < n; i++) {
if (vis[i]) continue;
ts[d] = a[i];
ans1 = ans;
for (int j = 0; j < d; j++) {
if (mp[ts[j]][ts[d]]) {
ans1 = max(ans1, d - j);
}
}
if (ans1 >= MIN) return ;
vis[i] = 1;
dfs(d+1, ans1);
vis[i] = 0;
}
return ;
}
int main()
{
while (readin()) {
sort(a, a+n);
// for (int i = 0; i < n; i++) printf("%c ", a[i]); puts("");
MIN = 10;
memset(vis, 0, sizeof vis);
dfs(0, 0);
for (int i = 0; i < n; i++) printf("%c ", num[i]);
printf("-> %d\n", MIN);
memset(mp, 0, sizeof mp);
}
return 0;
}
枚举二叉树
1.UVA1354天平难题——枚举二叉树
参考链接
解题思路:首先,模型是一棵二叉树,所有的叶子结点是物品(本题为挂饰),权值是相应的重量,其余非叶子结点都是吊环,就是天平杠杆的支点(非物品),重量是左右孩子的权值之和。也就是说,本题可以按照数组模拟树的方法,用深度优先搜素将该结点分别枚举为挂饰和吊环,之后将这棵树每个结点的左右宽度算出来。也就是先自顶向下构建树,之后自底向上计算并返回这种树的每个结点存储变量的数值,详细过程见代码注释。
(1) 枚举原则:每个结点有两种情况,叶子和非叶子,叶子是物品(挂饰),他的父亲结点必定是一个吊环,也就是非叶子结点。叶子结点的多少由非叶子结点的数量决定,每增加一个吊环,减少一个放置挂饰的位置,但会增加两个放置挂饰的位置。也就是说,如果增加一个吊环,那么就会多一个放置挂饰的位置,同时,这个放置挂饰的位置也可以增加吊环来保证最后有足够多的位置放置挂饰。所以,先有吊环,再有放置挂饰的位置,之后使用一个数组讲将所有非叶子结点做标记即可。
(2) 计算宽度:本题需要随时知道每个结点的左右子树的宽度,该节结点宽度是,左子树中最左边结点的坐标;右宽度是右子树中最右结点的坐标。如果真这么想,就错了!题目也有提醒:右子树的左宽度可能会高于左子树的左宽度。见下图的两种情况。
/*
2021.11.9 15:23 zzy
2021.11.9 16:37 zzy
*/
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e3+7;
double l[N], r[N], val[N], WIDTH, ans;
int w[N], vis[N], n; // w 挂饰重量,vis 枚举时标记挂饰是否被使用过
int tree[N];
/*存储树的数组:
(1)当前结点为空,因为其父亲结点为挂饰或者为空(放不了结点):0 ;
(2)挂饰:相应的挂饰序号 ;
(3)吊环:-1 .
*/
void cal(int x) {
memset(l, 0, sizeof l);
memset(r, 0, sizeof r);
memset(val, 0, sizeof val);
for (int i = x; i >= 1; i--) {
if (tree[i] == -1) {
int xx = (i<<1), yy = ((i<<1)^1);
val[i] = val[xx]+val[yy];
double L = val[yy]/(val[xx]+val[yy]);
double R = val[xx]/(val[xx]+val[yy]);
l[i] = max(l[xx]+L, l[yy]-R);
r[i] = max(r[xx]-L, r[yy]+R);
}
else if (tree[i]) {
val[i] = w[tree[i]];
// printf("%lf\n", val[i]);
}
}
// for (int i = 1; i <= x; i++)
// printf("%lf ", val[i]);
// puts("");
}
void dfs(int x, int loc, int num) {
/*
x: 结点序号
loc:当前树可放置吊环和挂饰的位置数量
num: 未放置的挂饰数量
*/
if (x == 1) { // dfs 入口
if (num == 1) { // 如果只有一个挂饰,直接返回宽度 0
ans = 0;
}
else {
tree[x] = -1; // 如果有超过1个的挂饰,那么根结点必定为吊环,一次扩充放置挂饰的位置
ans = -1;
dfs(x+1, loc+1, num);
}
return ;
}
// dfs 过程
if (num == 0) {
cal(x-1);
if (l[1]+r[1]-WIDTH > 1e-6) return ;
ans = max(l[1] + r[1], ans);
return ;
}
// cout << "x = " << x << endl;
if (tree[x>>1] != -1) { // 如果当前位置的父亲结点非吊环(因为没有位置),则直接跳过该结点.
dfs(x+1, loc, num);
return ;
}
// 枚举:
// 1. 该位置放置吊环
if (loc < num) {
tree[x] = -1;
dfs(x+1, loc+1, num); // 增加一个吊环,枚举下一个序号的结点
tree[x] = 0;
}
if (loc == 1 && num > 1) return ;
// 2. 该位置放置挂饰
for (int i = 1; i <= n; i++) {
if (vis[i]) continue;
tree[x] = i;
vis[i] = 1;
dfs(x+1, loc-1, num-1);
vis[i] = 0;
}
tree[x] = 0;
return ;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(vis, 0, sizeof vis);
scanf("%lf%d", &WIDTH, &n);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
dfs(1, 1, n);
if (ans == -1) puts("-1");
else printf("%.16lf\n", ans);
}
return 0;
}
解题思路2:枚举子集,分别将状态的左右两边当作左右孩子,以此类推,直到状体只剩一个1的时候,这时作为叶子结点,往回计算。
// 11.16 20.00 20.56
#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1e3+7;
int vis[N], a[N], n;
double l[N], r[N], w[N], m;
struct node {
double l, r;
node(){}
node(double l_, double r_): l(l_), r(r_) {}
};
vector<node> ns[N];
int bitcnt(int x) {
if (!x) return 0;
return bitcnt(x/2) + (x&1);
}
void dfs(int s) {
if (vis[s]) return ;
vis[s] = 1;
if (bitcnt(s) == 1) {
ns[s].push_back(node(0, 0));
return ;
}
for (int i = (s-1)&s; i; i = (i-1)&s) {
int ll = i, rr = i^s;
dfs(ll), dfs(rr);
for (auto nl: ns[ll]) {
for (auto nr: ns[rr]) {
double ld = w[rr]/(w[ll]+w[rr]);
double rd = w[ll]/(w[ll]+w[rr]);
double tld = max(nl.l+ld, nr.l-rd);
double trd = max(nr.r+rd, nl.r-ld);
// printf("%lf %lf\n", tld, trd);
ns[s].push_back(node(tld, trd));
}
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%lf%d", &m, &n);
memset(vis, 0, sizeof vis);
for (int i = 0; i < (1<<n); i++) ns[i].clear();
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 1; i < (1<<n); i++) {
w[i] = 0;
for (int j = 0; j < n; j++) {
if (i&(1<<j)) w[i] += a[j];
}
// printf("%d ", w[i]);
}
double ans = -1;
int s = (1<<n)-1;
dfs(s);
for (auto t: ns[s])
if (t.l+t.r - m <= 1e-6) ans = max(ans, t.l+t.r);
if (ans == -1) puts("-1");
else printf("%.16lf\n", ans);
}
return 0;
}
状态转移搜索、记忆化dfs,bfs
1.UVA10603倒水问题——记忆化搜索
思路分析:使用三维数组表示三个水杯的状态,由于三个水杯的水的总量不变,所以只需要二维数组表示状态。之后,通过一个水杯给另外一个水杯倒水的动作实现状态转移。
搜索方案:
(1)DFS:dfs 需要使用标记来避免不必要的搜索。具体标记方式为,每个状态用最少倒水量做标记,也就是说下一次搜索到这个状态的时候,如果倒水量大于当前标记的值,则不用搜索,因为当前的状态的水的消耗量更少。因为同样的状态能转移到的状态其安全相同,所以更坏情况下的不用考虑。
(2)BFS:第一种形式是直接使用队列维护当前状态即可,但是是否一定是最优解先无法证明,可以借鉴 Dijkstra 算法来完成分析。第二种方式是可以借鉴 DFS 的标记处理方式,即只要同一个状态的倒水消耗量更少,就可以再次进入队列,最后达到全部情况的搜索,而非第一种情况的每个状态只能进队一次。
/*
2021.11.9 22.45 zzy
2021.11.9 23.18 zzy
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e2+7;
int vis[N][N], state[3], cap[3], ansvol, ansd, d;
int ans[N];
bool upgrade(int d, int vol) {
if (ans[d] == -1 || ans[d] > vol) {
ans[d] = vol;
return true;
}
return false;
}
void dfs(int vol) {
int t = 1;
for (int i = 0; i < 3; i++) {
if (state[i] == d) {
ansd = d;
if (!upgrade(d, vol)) t = 0;
}
else if (ansd != d && state[i] < d) {
if (ansd == state[i]) {
if (!upgrade(ansd, vol)) t = 0;
}
else if (ansd < state[i]) {
ansd = state[i];
if (!upgrade(ansd, vol)) t = 0;
}
}
}
// if()
// printf("vol=%d\n", vol);
if (vis[state[0]][state[1]] && vis[state[0]][state[1]] <= vol) return ;
vis[state[0]][state[1]] = vol;
// if (!t) return ;
// for (int i = 0; i < 3; i++) printf("%d ", state[i]); puts("");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == j || !state[i] || state[j] == cap[j]) continue;
int ti = state[i], tj = state[j];
int mount = min(cap[j], state[i]+state[j])-state[j];
// if (vis[state[i]-mount][state[j]+mount]) continue;
state[i] -= mount, state[j] += mount;
// vis[state[i]][state[j]] = 1;
dfs(vol + mount);
// vis[state[i]][state[j]] = 0;
state[i] = ti, state[j] = tj;
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(ans, -1, sizeof ans);
memset(vis, 0, sizeof vis);
ansd = 0;
scanf("%d%d%d%d", &cap[0], &cap[1], &cap[2], &d);
state[0] = state[1] = 0, state[2] = cap[2];
dfs(0);
printf("%d %d\n", ans[ansd], ansd);
}
return 0;
}