竟然排名21,终于拿到一次奖品了奥利给(这次题好复杂,老铁可以多看看注释~)
知识点:模拟
初始时有numBottles个瓶酒,喝完之后每 numExchange 个瓶子可以兑换一瓶新酒,然后接着喝,喝完接着兑,直到手里的瓶子少于 numExchange 个。兑换过程中累计喝了多少瓶酒就完事了。
class Solution {
public:
int numWaterBottles(int numBottles, int numExchange) {
int sum = numBottles; //一开始喝了 numBottles 瓶酒
while(numBottles >= numExchange) {
int tmp = numBottles / numExchange; //兑换
numBottles -= numExchange*tmp;
sum += tmp; //把兑换的酒喝了
numBottles += tmp; // 得到了 tmp 个新瓶子,继续尝试兑换
}
return sum;
}
};
知识点:深度优先搜索
对于节点 x,如果我们知道以 x 为根节点的子树中各个字符的出现的次数(假设这些数据存储在一个map中,key 为字符,value 为出现次数),那么我们可以轻易的知道该子树中与 x 节点有相同字符的节点数(拿着x节点的字符去 map 查一下就ok了)。
如果对于每个节点我们都有这样的数据,那就太棒了,直接遍历每个节点,然后累加一下就O了。
那么如何拿到这些 map 呢?
如果 x 节点是叶子节点,那么这个map就太好构造了(毕竟只有一个节点)。
如果 x 节点不是叶子节点,那么可以通过其子节点的map来构造——累加各个子节点的map,然后再加上 x 节点自己的(子节点的map如何获得呢?递归啊老铁!)。
class Solution {
public:
vector<int> next[100000]; // 邻接表
int anw[100000]; // 用来放答案的数组
//dfs 函数的目的:递归的计算每个几点的map,并维护答案数组。
// ---
//root: 当前正在被处理的节点;
//pre: root 的父节点,如果root为根节点,则 pre = -1
//cnt: pre 的 map
void dfs(int root, int pre, const std::string &labels, int *cnt) {
int newcnt[26] = {0}; // newcnt 是 root 节点的 map
int pos = labels[root] - 'a';
newcnt[pos] += 1;
for(auto nex : next[root]) { // 遍历子节点
if(nex == pre) {
continue;
}
dfs(nex, root, labels, newcnt); // 计算 newcnt
}
if (cnt != nullptr) {
for(int i = 0; i < 26; i++) { // 用 newcnt 更新下父节点的 map
cnt[i] += newcnt[i];
}
}
anw[root] = newcnt[pos]; // 计算答案
}
vector<int> countSubTrees(int n, vector<vector<int>>& edges, string labels) {
// 先把边表转成邻接表
for(const auto &edge : edges) {
next[edge[0]].push_back(edge[1]);
next[edge[1]].push_back(edge[0]);
}
// 初始化一下
memset(anw, 0, sizeof(int)*n);
//开始递归
dfs(0, -1, labels, nullptr);
return vector<int>(anw, anw+n);
}
};
知识点:BFS;动态规划
个人感觉这个题比第四道题难啊,都快给我写吐了。
首先要读明白一句话:如果一个子字符串包含字符 c ,那么 s 中所有 c 字符都应该在这个子字符串中。
这句话里包含了一个递归的潜规则——如果一个子串包含 c,那么该子串必须包含所有的 c,又因为这些 c 中间可能插入了其他的字符 h,所以该子串也应该包含所有的 h,进而 h 中间还可能插入了 a,所以该子串中也应该包含所有的 a,依次类推,直到不会引入新的字符类别!
想明白了这个潜规则就能得出子串个数不会超过26的结论。26字母有序排列,不互相掺杂,这个时候可以划分出26个子串。只要字符互相掺杂,就必然会使子串数量减少。比如,aabbcc 最多可以划分出三个子串(aa,bb,cc),abbacc 最多只能划分出两个(abba,cc 或者 bb,cc)。
接下来,我们需要处理出每个子串的起始位置。我们可以从子串必须包含某个特定字符入手,把这个特定字符作为递归的潜规则的入口,进而得到整个子串的起点和终点。
比如,现在有字符串 abccbadd:
有了每个子串的起点和终点,我们就要开始最后一步处理了——动态规划 (已吐) 。
设 dp[i] 为前i个字符中的最优解。(因为最优结果是二维的,包含长度 len 和个数 cnt ,所以dp[i]应该是个结构体)
首先来考虑下初始状态,在前 0 个字符,即空串中的最优解为 dp[0] = {cnt : 0, len: 0}。
接下来处理第一个字符,如果该字符串是某个子串的结尾,那么:
dp[1] = {cnt : 1, len : 1}
如果该字符不是某个子串的结尾,那么 dp[1] = dp[0],因为以该字符为结尾无法构成一个合法的子串。
推广到第 i 个子串,如果第 i 个字符是某个子串的结尾,设该子串为 str,其长度为 len,那么 dp[i] = better(dp[i-1], dp[i-len] + str) 。
否则,dp[i] = dp[i-1]。
现在我们有了最优解的长度和子串个数,然后我们可以通过状态转移的过程倒推出各个子串。
class Solution {
public:
struct Node {
int cnt; // 子串个数
int len; // 子串总长度
int pre; // 当前状态中最后一个子串的其实位置
Node() : cnt(0), len(0), pre(0) {}
};
Node dp[100001];
int left[26]; // 第 i 种字符最左边的位置
int right[26]; // 第 i 种字符最右边的位置
unordered_set<int> inc[26]; // left[i] 和 right[i] 之间包含的字符种类
int mleft[26]; // 以第 i 种字符为入口的子串的起始位置
int mright[26]; // 以第 i 种字符为入口的子串的终点位置
// 以 goal 字符为入口,计算mleft[goal] 和 mright[goal]
// 本质上是个 bfs
void update(int goal) {
int &L = mleft[goal];
int &R = mright[goal];
L = 1000000000;
R = -1;
queue<int> q;
q.push(goal);
bool mark[26] = {false};
mark[goal] = true;
while(q.empty() == false) {
int f = q.front();
q.pop();
L = min(left[f], L);
R = max(right[f], R);
for(auto it = inc[f].begin(); it != inc[f].end(); ++it) {
if(mark[*it] == false) {
mark[*it] = true;
q.push(*it);
}
}
}
}
vector<string> maxNumOfSubstrings(string s) {
memset(left, 0x7f7f, sizeof(left));
memset(right, 0, sizeof(right));
// 计算 left 和 right
for(int i = 0; i < s.size(); i++) {
int pos = s[i]-'a';
left[pos] = min(left[pos], i+1);
right[pos] = max(right[pos], i+1);
}
// 计算 inc
for(int i = 0; i < 26; i++) {
for(int l = left[i]-1; l < right[i]; l++) {
if(inc[i].find(s[l]-'a') == inc[i].end()) {
inc[i].insert(s[l]-'a');
}
}
}
// 计算 mleft mright,即每个子串的起始位置和终点位置。
for(int i = 0; i < 26; i++) {
update(i);
}
//动态规划求解最优解
for(int i = 1; i <= s.size(); i++) {
int pos = s[i-1]-'a';
if(mright[pos] == i) {
Node tmp;
tmp.cnt = dp[mleft[pos]-1].cnt + 1;
tmp.len = dp[mleft[pos]-1].len + mright[pos] - mleft[pos]+1;
tmp.pre = mleft[pos]-1; //记录一下当前子串的起始位置
// 选择较优的一种策略,子串数量更多或者长度更短
if(tmp.cnt > dp[i-1].cnt
|| (tmp.cnt == dp[i-1].cnt && tmp.len < dp[i-1].len)) {
dp[i] = tmp;
} else {
dp[i] = dp[i-1];
}
} else {
dp[i] = dp[i-1];
}
}
//通过 pre 构造答案
int pos = dp[s.size()].pre;
int sum = dp[s.size()].len;
vector<string> anw;
do {
int L = pos+1;
int R = mright[s[L-1]-'a'];
string str = s.substr(L-1, R-L+1);
sum -= str.size();
anw.push_back(std::move(str));
pos = dp[pos].pre;
}while(sum > 0);
return anw;
}
};
知识点:模拟退火;三分
首先来看看 func(arr, l, r) 到底在搞啥子。原来就是返回 arr[l…r] 子区间的与值啊。
这里还是有个潜规则:固定 l,然后随着 r 的增加,func(arr, l, r) 是单调非递增的!
一个单调非递增函数和一个常量做减法还是一个单调非递增函数。
那一个单调非递增函数取绝对值呢,要么还是单调非递增的,要么就变成了一个凹函数!
那问题的答案就已经呼之欲出了!枚举 l,然后模拟退火求出一个最优 r。这样就得到了 n 个局部最优的区间 [l,r],在这些区间中最优的那个就是答案了!
这玩意比上个题简洁多了啊
class Solution {
public:
//通过预处理,快速求解arr[L..R]的与值
int pre[100001][20] = {0};
int get(int L, int R, int target) {
int val = 0;
for(int i = 0, bit = 1; i < 20; i++, bit <<= 1) {
// 如果第 i 个bit 在 [L,R] 中全为 1,那么与值的该bit也必然为 1。
if(pre[R][i] - pre[L-1][i] == R-L+1) {
val |= bit;
}
}
return abs(val-target);
}
// 用模拟退火求解关于 L 的局部最优解
int query(int L, int n, int target) {
int dir[2] = {-1, 1}; // 两个方向
int step = 1000; // 初始步长
int now = L; // R 的起始位置
int best = 100000000; // 局部最优解
while(step > 0) {
int Lpos = now + step*dir[0];
if(Lpos < L) {
Lpos = L;
}
int Rpos = now + step*dir[1];
if(Rpos > n) {
Rpos = n;
}
// 向左右两个方向各走一步,求值
int ldis = get(L, Lpos, target);
int rdis = get(L, Rpos, target);
int pbest = best;
//更新位置及最优解
if(ldis < best) {
now = Lpos;
best = ldis;
}
if(rdis < best) {
now = Rpos;
best = rdis;
}
//如果没有找到更优解,那就缩小步长
if(pbest == best) {
step /= 2;
}
}
return best;
}
int closestToTarget(vector<int>& arr, int target) {
int anw = 100000000;
//统计前 i 个数字中,第 j 个bit 为 1 的数量。
for(int i = 0; i < arr.size(); i++) {
for(int j = 0, bit = 1; j < 20; j++, bit <<= 1) {
pre[i+1][j] = pre[i][j] + ((bit&arr[i]) ? 1 : 0);
}
}
for(int i = 1; i <= arr.size(); i++) {
anw = min(anw, query(i, arr.size(), target));
}
return anw;
}
};