果然是力扣杯,难度较于平时周赛提高了不少,个人感觉最后两题并不太容易QAQ
LCP 18.早餐组合 #二分思想
题目链接
题意
你获得了每种主食的价格,及每种饮料的价格,你需要选择一份主食和一份饮料,且花费不超过(x)元。现要求购买方案数。
分析
先分别对主食与饮料进行排序。枚举主食的价格,得出饮料最高价格,再二分寻找这一价格对应的饮料编号,小于该编号的饮料均能够与当前主食进行搭配。同理,枚举饮料价格,再二分对应主食价格。时间复杂度为(O(nlogn))。速度并不快,可以用空间换取时间,用哈希表去维护。
typedef long long ll;
const int MOD = 1e9 + 7;
class Solution {
public:
int breakfastNumber(vector<int>& staple, vector<int>& drinks, int x) {
sort(staple.begin(), staple.end());
sort(drinks.begin(), drinks.end());
ll lans = 0, rans = 0;
for (int i = 0; i < staple.size(); i++) {
int last = x - staple[i];
if (last <= 0) continue;
int pos = upper_bound(drinks.begin(), drinks.end(), last) - drinks.begin() - 1 + 1;
lans += (ll)pos;
}
for (int j = 0; j < drinks.size(); j++) {
int last = x - drinks[j];
if (last <= 0) continue;
int pos = upper_bound(staple.begin(), staple.end(), last) - staple.begin() - 1 + 1;
rans += (ll)pos;
}
return max(lans % MOD, rans % MOD);
}
};
LCP 19.秋叶收藏集 #线性DP
题目链接
题意
有一份秋叶收藏集leaves
,是含有红叶、黄叶的排列。你需要将收藏集中部分秋叶进行替换,使得其中排列成“红、黄、红”三部分,而每部分树叶数量可不相等且均需大于等于1
。每次替换,可将红叶替换为黄叶,或者将黄叶替换为红叶。现要求出上述排列的最小调整次数。
分析
其实该题并不难,但是好久没有碰到线性(DP)了,故此处参考了官方题解。既然题目要求“红、黄、红”三部分,显然前面红色与后面红色状态不同,故我们可对部分秋叶分成三种状态。当状态为1
,表示当前秋叶处于黄色部分;当状态为0
或2
,表示当前秋叶处于前面红色部分或后面红色部分。定义(dp[i][j]),表示对第(0)片到第(i)片叶子进行调整,且第(i)片叶子恰好处于状态(j)时所需的最小操作次数。
- 当(j=0),说明第(i)片叶子处于前面红色部分,说明了第(i)片叶子应该为红色,且第(i-1)片叶子只能是红色。状态转移方程有:(dp[i][0]=dp[i-1][0] + (leaves[i] != ‘r’))(当第(i)片叶子不为红色时,需要调整一次)
- 当(j=1),说明第(i)片叶子处于中间黄色部分,说明了第(i)片叶子应该为黄色,但要注意,该片叶子既有可能处于“红-黄”的过渡状态,又可能处于“黄-黄”的中间状态。故第(i-1)片叶子的状态(j_{i-1})可以取(0 or 1)。由于我们当前时刻,只关注于第(i)片叶子为黄色的条件下,因此此处局部的最小值,也是整体的最小值构成之一,因而状态转移时取(j=0)及(j=1)的最小操作次数的最小值:(dp[i][1] = min{dp[i-1][0], dp[i-1][1]}+(leaves[i] != ‘y’))(当第(i)片叶子不为黄色时,需要调整一次)
- 当(j=2),说明第(i)片叶子处于前面红色部分,说明了第(i)片叶子应该为红色,但要注意,该片叶子既有可能处于“黄-红”的过渡状态,又可能处于“红-红”的连续状态。故第(i-1)片叶子的状态(j_{i-1})可以取(1 or 2)。转移方程有:(dp[i][1] = min{dp[i-1][1], dp[i-1][2]}+leaves[i] != ‘r’) 注意,要转移该状态,必须满足(igeq2),即保证至少之前有一片叶子属于“前面红色”、有一片叶子属于“黄色”。
初始情况下为任何状态的值赋予无穷大。对于(i=0),(j)只能为(0),故边界条件为(dp[0][0]=(leaves[0] != ‘r’))
int dp[100005][3];
class Solution {
public:
int minimumOperations(string leaves) {
int len = leaves.length();
for (int i = 0; i < len; i++) dp[i][0] = dp[i][1] = dp[i][2] = 0x3f3f3f3f;
dp[0][0] = (leaves[0] != 'r');
for (int i = 1; i < len; i++) {
dp[i][0] = dp[i - 1][0] + (leaves[i] != 'r');
dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]) + (leaves[i] != 'y');
if (i >= 2) dp[i][2] = min(dp[i - 1][1], dp[i - 1][2]) + (leaves[i] != 'r');
}
return dp[len - 1][2];
}
};
LCP 20.快速公交 #记忆化搜索 #数学 #逆向思维
题目链接
题意
假定你起始站点在(0),秋日市集站点为(target)。你如果从沿途中(x)号站点步行到(x+1)号站点,需花费(inc);如果从(x)号站点步行到(x-1)号站点,需花费(dec)。现有(m)辆公交车(编号从(0)开始),你可以任意次数搭乘编号为(i)的公交车,由此能够从(x)号站点移动至(x imes jump[i])号站点,而耗时为(cost[i])。现要求出到达(target)最少花费时间,对(1e9+7)取模。移动过程中可越过(target)站点。
分析
与以往记忆化搜索不太一般,中间过程中需要一点点数学公式推导,从而保证搜索的状态数不会太多。
如果你从(0)号出发去搜索的话,你可能需要每移动一步就要搜索一下是否要用到公交车。但反过来,如果你从终点(target)到(0)号相反的方向去思考,已经知道现在到达的站点编号,欲要求上一站点编号,可以通过编号与公交车移动跨度的整除关系去判断之前是否要坐公交车。感谢@Zhenghao-Liu的思路。
从起点(0)到终点,无疑就是步行与公交的组合,有四种决策:
-
直接全部步行
-
直接坐公交
-
先向前走几步再乘公交
-
先向后走几步再乘公交
我们把方向逆转一下,假设当前到达站点编号为(curpos),我们要找到上一站点编号(pre)。我们依次枚举每一辆公交(i),决策(1)容易求,同时决策(1、2)也属于决策(3、4)的子集。那么我们分析下面两个决策:
- 先向前走几步再乘公交。方向逆转一下就是,也就是(pre)号站点先搭乘公交(i),到达第(pre imes jump[i])号站点,再步行(step_1)个站点,到达(curpos),即(pre imes jump[i] + step_1=curpos)。显然(curpos\%jump[i])得到(step_1),(curpos/jump[i])得到(pre)号站点
- 先向后走几步再乘公交。即(pre imes jump[i]-step_2=curpos),但左边等式出现减号,为了方便求出余数,我们不妨令(-step_2=-jump[i]+step_3),此时等式变为(pre imes jump[i]-jump[i]+step_3=(pre-1) imes jump[i] + step_3=curpos)。显然(curpos\%(pre+1))可求得(step_3),即(jump[i]-curpos\%(pre+1))可求得(step_2)。而(curpos/jump[i]+1)可求得(pre)。
这里要注意下,对于决策四,当(curpos==jump[i]==2)时,如果不用第(20)行代码中(curpos % jump[i] != 0)
条件限制,会导致prepos = curpos / jump[i] + 1;
一直使得(prepos)一直为(2)的死循环
typedef long long ll;
class Solution {
private:
const int MOD = 1e9 + 7;
const ll INF = 0x3f3f3f3f3f3f3f3f;
unordered_map<int, ll> dp;
int inc, dec, target, n;
vector<int> jump, cost;
public:
ll DFS(int curpos) {
if (curpos == 1) return dp[1] = inc;
if (curpos == 0) return dp[0] = 0;
if (dp.find(curpos) != dp.end()) return dp[curpos];
ll rev = (ll)curpos * inc;
for (int i = 0; i < n; i++) {
ll step_1 = curpos % jump[i]; //曾经前进过一段距离
ll prepos = curpos / jump[i]; //曾经坐上公交车的站台
rev = min(rev, step_1 * (ll)inc + cost[i] + DFS(prepos));
if (curpos % jump[i] != 0) { //说明curpos不被整除,不会出现死循环
//(pre - 1) * jump + (jump - step_2) = curpos
ll step_2 = jump[i] - curpos % jump[i];
prepos = curpos / jump[i] + 1;
rev = min(rev, step_2 * (ll)dec + cost[i] + DFS(prepos));
}
}
return dp[curpos] = rev;
}
int busRapidTransit(int target, int inc, int dec, vector<int>& jump, vector<int>& cost) {
this->target = target; this->inc = inc; this->dec = dec;
this->n = jump.size(); this->jump = jump; this->cost = cost;
dp.clear();
return DFS(target) % MOD;
}
};
LCP 21. 追逐游戏 #深搜找环 #宽搜求最近环入口
题目链接
题意
(N)个顶点(从(1)计数),有(N)条路使得任意两个顶点可互相到达,且不存在重边。站在点上的A希望在最快时间追上站在另一点的B,而B希望尽可能延后被A追上的时间。每一回合,A先行动,B观察A的行动后再行动,上述行动是指要么留在原地,要么移动至相邻顶点。而两人一定采取最优移动策略,若A能追上B,则游戏结束,返回最少回合数;若A无法追上B,返回-1
。
分析
(N)个节点且(N)条边的连通图,一定有且仅有一个环。
感谢@lucifer1004的思路与代码,真的十分清晰。
若当A、B一开始即相邻,A第一回合(B还未动)就追上了B,即A赢。
否则,我们先从该图中找到这唯一的环,对环上的点进行标记。(利用DFS及DFS序)
- 若环长度(geq4),两人均有机会赢,如果B能在A拦截之前进入该环,无论怎么绕,A都无法再追上B,此时我们就要为B找到其最近的环入口,如果B到该逃脱入口的距离(+1) 小于 A到达该逃脱入口(利用BFS计算),B没有被A拦截,一定不会被追上。否则,A赢。
- 若环长度为(3),A和B同处该环时,A一定能够追上B,即A赢。
当上述两种A赢情况成立时,接下来就要求出最少回合数。枚举B最终被追上的位置(此时一定满足 B到该点的距离(+1) 大于等于 A到达该点的距离 ),由于B为了拖长回合数,一定会挑选距离更长的 结束点。当我们求出B在最优情况下选择的距离最长的点时,对应地就是我们所要求的的最少回合数了。
const int MAXN = 100005;
class Solution {
private:
vector<int> H[MAXN];
int depth[MAXN] = { 0 }, fa[MAXN] = { 0 }, n;
int looplen = 0, ans = 0;
bool isLoop[MAXN] = { false };
public:
vector<int> BFS(int start, bool findLoop) {
//findLoop为true时求最近环入口点,为false时求start到所有点的最短距离
vector<int> dis(n + 1, 0x3f3f3f3f);
queue<int> myque;
dis[start] = 0; myque.push(start);
while (!myque.empty()) {
int u = myque.front(); myque.pop();
if (findLoop && isLoop[u]) return { u, dis[u] }; //找到最近的环入口,返回入口编号及距离
for (int i = 0; i < H[u].size(); i++) {
int v = H[u][i];
if (dis[v] > dis[u] + 1) {
dis[v] = dis[u] + 1;
myque.push(v);
}
}
}
return dis;
}
void DFS(int u, int parent) {
fa[u] = parent;
depth[u] = depth[parent] + 1;
for (int i = 0; i < H[u].size(); i++) {
int v = H[u][i]; if (v == parent) continue; //避免无用的回边
if (depth[v] == 0) DFS(v, u);
else if (depth[v] < depth[u]) { //v的DFS序 比 u的DFS序 更早,说明找到反向边。即有环
isLoop[v] = true; looplen++;
int tmp = u;
while (tmp != v) { //找到之前经过路径的v,并在该路径中的经过顶点做标记
isLoop[tmp] = true;
looplen++;
tmp = fa[tmp]; //回溯
}
}
}
}
int chaseGame(vector<vector<int>>& edges, int startA, int startB) {
n = edges.size();
for (int i = 0; i < n; i++) {
int u = edges[i][0], v = edges[i][1];
H[u].push_back(v); H[v].push_back(u);
if (u == startA && v == startB) return 1;//相邻节点的话A第一步直接抓到B
if (v == startA && u == startB) return 1;
}
DFS(1, 0); //找到环并对该环上的所有顶点进行标记
vector<int> disFromA = BFS(startA, false);
vector<int> disFromB = BFS(startB, false);
if (looplen >= 4) { //B有机会逃脱
vector<int> tmp = BFS(startB, true);
//通过BFS,找到B逃进环的最近入口。tmp[0]为环入口,tmp[1]为B到该入口的距离
if (disFromA[tmp[0]] > tmp[1] + 1)
return -1; //A到入口距离 应大于 (B到入口距离+1) ,B才顺利逃进环中
//否则 若A到入口距离 刚好为 (B到入口距离+1) 时,B到达入口后下一步A就能抓住B
}
//B不能成功进环 或者 环的阶数为三,B注定要被A追上了,那么B一定要选个距离长的点
for (int i = 1; i <= n; i++) {
if (disFromA[i] > disFromB[i] + 1) ans = max(ans, disFromA[i]);
}
return ans;
}
};