開學前的最后一次做算法題吧,不知道開學后的復賽有沒有時間打了
不過能拿到一件阿里的紀念T恤還是蠻開心的,賽題也很友好,適合我這種養老選手~
樹木規划
描述
在一條直的馬路上,有 \(n\) 棵樹,每棵樹有一個坐標,代表它們距離馬路起點的距離。 如果每相鄰的兩棵樹之間的間隔不小於\(d\) ,那么我們認為這些樹是美觀的。 請計算出最少移除多少棵樹,可以讓這些樹變得美觀。
樹木的棵樹為 \(b\),\(1\le n\le 10^5\)。 樹木的坐標用 \(trees\) 表示,\(0\le tresss_i\le 10^9\)。 最小美觀間隔為 \(d\) ,\(1\le d\le 10^9\)。 保證輸入的坐標是排好序的,且兩兩不相同。
說明
樣例中,將位置 \(2\) 和 \(6\) 的樹木移走,剩下 \([1,3,5]\) ,它們是美觀的。
示例
輸入:
[1,2,3,5,6]
2
輸出:
2
——————————————————————————————————
第一道難度不會太大,解題目標是 “移除樹數量最小” ,換言之就是 “保留樹數量最大”
於是很自然想到首先保存每棵樹的原定坐標到數組,然后貪心地遍歷保留各種較多的樹
貪心的策略是:首先選取最靠左的樹,然后在美觀的條件下向右選擇最近的樹並保留
代碼如下
class Solution {
public:
/**
* @param trees: the positions of trees.
* @param d: the minimum beautiful interval.
* @return: the minimum number of trees to remove to make trees beautiful.
*/
int treePlanning(vector<int> &trees, int d) {
int n=(int)trees.size(),cnt=1,now=trees[0];
for (int i=1;i<n;i++)
if (now+d<=trees[i]) cnt++,now=trees[i];
return n-cnt;
}
};
正三角形拼接
描述
給出 \(n\) 根木棍,每次切割可以將 \(1\) 根木棍切成 \(2\) 段。請計算出最少切割幾次,可以從所有木棍中選出 \(3\) 根,組成一個正三角形。
一開始的木棍根數為 \(n,3 ≤ n ≤ 1000\) 。所有木棍的長度為一個整型數組 \(lengths,1 \le length_i ≤ 10^9\) 。
切割必須要將木棍分成 \(2\) 根整數長度的木棍,而且總長度要和原木棍相等。
說明
可以從長為 \(7\) 的木棍中,切出 \(2\) 根長為 \(3\) 的木棍,那么木棍的長度應該為 \([2,3,1,3,3,5]\) ,可以拼出邊長為 \(3\) 的正三角形。
示例
輸入:
[2,3,7,5]
輸出:
2
——————————————————————————————————
對簡單思維能力的考察,首先注意到 \(n ≥ 3\),而顯而易見三根以上的木棍至多切割兩次就可以湊出正三角(以最短木棍長為邊長)
由於切割情況只有零次、一次、兩次,我們可以從簡單的開始下手
首先是零次,需要初始就有三根等長的木棍,這個判斷可以在遍歷中完成(cnt 為計數變量)
然后考慮一次,有以下兩種情況:
(1)初始有兩根等長的木棍,並且任意存在一根比它們長的木棍
(2)對半切割某偶數長度的木棍后正好能與另一長度為其一半的木棍湊出(例如 [3,6] -> [3,3,3] )
其他情況均需要兩次切割
確定了思路之后,代碼就不難按個人習慣寫出了,要注意下各種細節就好了
代碼如下
class Solution {
public:
/**
* @param lengths: the lengths of sticks at the beginning.
* @return: return the minimum number of cuts.
*/
int makeEquilateralTriangle(vector<int> &lengths) {
int n=(int)lengths.size(),cnt=1,book=0;
sort(lengths.begin(),lengths.end()); //首先進行排序
for (int i=1;i<n;i++) {
if (lengths[i]==lengths[i-1]) cnt++;
else cnt=1;
if (cnt==3) return 0;
if (cnt==2 && i!=n-1) book=1;
if (!(lengths[i]&1))
for (int j=0;j<i;j++)
if (lengths[j]*2==lengths[i]) book=1;
}
if (cnt==3) return 0;
else if (book) return 1;
else return 2;
}
};
大樓間穿梭
描述
蜘蛛俠在大樓間穿梭。大樓的高度可以看作是一個從左到右排列的數組。
現在蜘蛛俠站在第一棟大樓上,他想跳到最后一棟上。
蜘蛛俠的視野為 \(k\),他可以花費 \(x\) 點體力,用蛛絲移動到右側 \(k\) 幢建築中第一棟比當前位置高的大樓
(此處題目有問題,應是大於等於當前高度的大樓)。
或者蜘蛛俠可以花費 \(y\) 點體力,跳躍到右側接下來兩棟大樓其中一棟。
請計算蜘蛛俠最少花費多少體力,到達最后一棟上。
大樓的高度為數組 \(heights\),一共有 \(n\) 棟大樓,\(2 ≤ n ≤ 10^5,1 <= heights_i ≤ 10^9\);
蜘蛛俠的視野為 \(k\),\(1 ≤ k ≤ n\)。兩種行動的體力花費滿足 \(1 ≤ x,y ≤ 10^9\)。
說明
樣例中,先花費 \(6\) 點體力跳到第三棟建築,再花費 \(10\) 點到達最后一棟。
示例
輸入:
heights = [1,5,4,3,3,5]
k = 3
x = 10
y = 6
輸出:
16
——————————————————————————————————
開口就是老最優化問題了,題意也很清晰,可以直接上 DP
若 \(nxt[i]\) 記錄了 \(i\) 樓之后第一座高度大於等於 \(i\) 的樓號,則可以直接狀態轉移
由於使用的是刷表法,所以直接用代碼來表示比較方便
w[0]=0;
for (int i=0;i<n;i++) {
w[i+1]=min(w[i+1],w[i]+y);
w[i+2]=min(w[i+2],w[i]+y);
if (nxt[i]!=-1) w[nxt[i]]=min(w[nxt[i]],w[i]+x);
}
但是問題在於 \(nxt[]\) 數組如何生成,若是使用朴素的 \(O(n^2)\) 在隨機數據下大概率能過(期望復雜度大概 \(O(nlogn)\)),
但是這道題數據是刻意構造過的,會 TLE,所以我這里用的是單調隊列來優化)
我們維護一個以坐標為第一關鍵字,高度為第二關鍵字的單調隊列,即
對於隊列內任意靠向隊首的元素 \(i\),靠向隊尾的元素 \(j\),滿足:
\(position_i < position_j\;,\;\;\;height_i < height_j\)
且任意時刻隊列元素個數小於等於 \(k\),在遍歷時要從后向前,具體處理方式同一般的單調隊列
這樣就能用 \(O(n)\) 的時間處理好 \(nxt[]\) 數組了,加上前述的 DP 本題就完成了
代碼如下
class Solution {
public:
/**
* @param heights: the heights of buildings.
* @param k: the vision.
* @param x: the energy to spend of the first action.
* @param y: the energy to spend of the second action.
* @return: the minimal energy to spend.
*/
long long shuttleInBuildings(vector<int> &heights, int k, int x, int y) {
long long w[100007];
int n=(int)heights.size(),nxt[100007];
deque<int> q1,q2;
for (int i=0;i<100007;i++) w[i]=9223372036854775807;
for (int i=n-1;i>=0;i--) {
if ((!q2.empty()) && i+k<q2.back())
q1.pop_back(),q2.pop_back();
while ((!q1.empty()) && q1.front()<heights[i])
q1.pop_front(),q2.pop_front();
nxt[i]=q2.empty()?-1:q2.front();
q1.push_front(heights[i]),q2.push_front(i);
}
w[0]=0;
for (int i=0;i<n;i++) {
w[i+1]=min(w[i+1],w[i]+y);
w[i+2]=min(w[i+2],w[i]+y);
if (nxt[i]!=-1) w[nxt[i]]=min(w[nxt[i]],w[i]+x);
}
return w[n-1];
}
};
對稱前后綴
描述
給定一個字符串 \(s\)。我們令一個字符串的權值為一個字符串的最長對稱前后綴長度。
請求出 \(s\) 的所有子串的權值的總和。
例如,“abcxyzcba” 的最長對稱前后綴的長度為 \(3\) ,因為 “abc” 和 “cba” 對稱。
字符串的長度為 \(n\),\(1 ≤ n ≤ 3*10^3\)。字符串均由小寫英文字符組成。
說明
樣例中,單個字符的子串的權值為 \(1\) ,它們的和為 \(7\)。
另外的權值為:"bacb"->1,"bacbdab"->2,"bdab"->1,"acbda'->1,所以權值和為 \(12\) 。
示例
輸入:
bacbdab
輸出:
12
——————————————————————————————————
考慮到子串前后綴的對稱性,可以用區間 DP 來解決
用 \(w[i][j]\) 表示子串 \([i,j]\) 的最長對稱前后綴長度,
從小區間開始向大區間擴展,\(w[i][j] \leftarrow w[i+1][j-1]\)
轉移的條件是 \(s[i]==s[j]\),具體有兩種情況:
(1)中間未被隔開,前后綴重合,\(w[i][j]=w[i+1][j-1]+2\)
(2)中間被隔開,前后綴無交集,\(w[i][j]=w[i+1][j-1]+1\)
另外使用 \(vis[][]\) 數組標記區間中是否被非對稱元素隔開(並在轉移中繼承下去),具體細節請見代碼
class Solution {
public:
/**
* @param s: a string.
* @return: return the values of all the intervals.
*/
long long suffixQuery(string &s) {
long long w[3007][3007];
bool vis[3007][3007];
long long len=(long long)s.size(),ans=len;
memset(w,0,sizeof(w));
memset(vis,0,sizeof(vis));
for (int i=0;i<len;i++) w[i][i]=1;
for (int k=2;k<=len;k++) {
for (int i=0;i<=len-k;i++) { //[i,i+k-1]
int j=i+k-1;
if (s[i]==s[j]) {
if (!vis[i+1][j-1]) w[i][j]=w[i+1][j-1]+2;
else {
w[i][j]=w[i+1][j-1]+1;
vis[i][j]=true;
}
}
else vis[i][j]=true;
ans+=w[i][j];
}
}
return ans;
}
};
可愛的彩蛋:毛毛太強啦,這都能發現