目次
1. コンセプト
- 貪欲なアルゴリズム (貪欲なアルゴリズムとも呼ばれます) は、問題を解決するときに、常に現時点で最良の選択を行うことを意味します。つまり、全体最適性を考えずに作られるのは、ある意味で局所最適解です。
- 貪欲な選択とは、求める問題の全体的な最適解が、一連の局所的な最適な選択、つまり貪欲な選択によって達成できることを意味します。これは貪欲なアルゴリズムが機能するための最初の基本要素です
- 問題の最適解がその部分問題の最適解を含む場合、その問題は最適な部分構造の性質を持っていると言われます。貪欲な戦略は、すべての変換で最適解を取得するために使用されます。問題の最適部分構造特性は、貪欲なアルゴリズムによって問題を解決できるという重要な特性です。貪欲なアルゴリズムのすべての操作は、結果に直接影響します。貪欲なアルゴリズムは、各部分問題の解決策を選択し、元に戻すことはできません
- 貪欲なアルゴリズムの基本的な考え方は、問題の初期解から段階的に進めることです. 特定の最適化尺度に従って、各ステップは局所的な最適解が得られることを保証する必要があります. 各ステップで考慮されるデータは 1 つだけであり、その選択は局所最適化の条件を満たす必要があります。次のデータと部分最適解の間の接続が実行可能な解でなくなった場合、すべてのデータが列挙されるまで、データは部分解に追加されません。または、データを追加できなくなるとアルゴリズムが停止します。
- 実際には、貪欲なアルゴリズムが適用されるケースはほとんどありません。一般に、ある問題が欲張りアルゴリズムに適しているかどうかを分析するには、まず問題の下にある実際のデータをいくつか選択して分析し、その後で判断することができます。
貪欲なアルゴリズムの問題:
- 得られた最終的な解が最適であるという保証はありません
- 最大値または最小値を見つけるために使用できない問題
- 特定の制約を満たす実行可能な解の範囲のみを見つけることができます
2. 選択ソート
難易度:簡単
選択ソートは貪欲な戦略を使用します。それが採用する貪欲な戦略は、ソートされていないデータから毎回最小値を選択し、ソートされていないデータが0になるまでソートされていないデータの開始位置に最小値を置き、ソートが終了することです。
#include <iostream>
void Swap(int* num1, int* num2)
{
if (num1 == num2) return;
*num1 += (*num2);//a = a+b
*num2 = (*num1) - (*num2);//b = a
*num1 -= (*num2);
}
void SelectSort(int* arr, int size)
{
for (int i = 0; i < size - 1; ++i)
{
int minIndex = i;
for (int j = i + 1; j < size; ++j)
{
if (arr[j] < arr[minIndex]) minIndex = j;
}
Swap(&arr[i], &arr[minIndex]);
}
}
int main()
{
int arr[] = { 10,8,6,20,9,77,32,91 };
SelectSort(arr, sizeof(arr) / sizeof(int));
for (int i = 0; i < sizeof(arr)/sizeof(int); ++i) {
std::cout << arr[i] << " ";
}
return 0;
}
3.バランスストリング
難易度:簡単
貪欲な戦略: ネストされたバランスを持たない, バランスに達する限り, すぐに分割されます.
したがって, 可変バランスを定義して、さまざまな文字に遭遇したときにさまざまな方向に変化することができます. バランスが0でバランスがに達すると、次の
ように分割数が更新されます。
文字列 s を左から右にスキャンし、L が検出された場合は Balance - 1、R が検出された場合は Balance + 1 します。残高が 0 の場合、レコード ++count を更新し、最後のカウント == 0 の場合、s をそのままにしておくだけでよいことを意味し、1 を返します
class Solution {
public:
int balancedStringSplit(string s) {
int count = 0;
int balance = 0;
for (int i = 0; i < s.size(); ++i) {
if (s[i] == 'L') --balance;
if (s[i] == 'R') ++balance;
if (balance == 0) ++count;
}
return count;
}
};
4. 株の売買に最適な時期
難易度:中
連続上昇取引日: 初日に買って最終日に売ると、毎日売買するのと同じように、最大の利益が得られます
.お金を失うことはありません。
したがって、株式取引日の価格表全体をトラバースし、すべての上昇取引日に売買し (すべての利益を得る)、すべての下降取引日に売買しない (決してお金を失うことはない) ことができます。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ret = 0;
for(int i = 1; i < prices.size(); ++i) {
int profit = prices[i] - prices[i - 1];
if(profit > 0) ret += profit;
}
return ret;
}
};
5.ジャンピングゲーム
難易度:中
配列内の任意の位置 y について、到達可能かどうかをどのように判断しますか? トピックの説明によると、位置 x がある限り、それ自体に到達でき、そのジャンプの最大長は x + nums[x] であり、この値は y 以上、つまり、 x+nums[x] ≥ y の場合、位置 y も到達できます
配列内の各位置を順番にトラバースし、リアルタイムで到達可能な最も遠い位置を維持します。現在通過している位置 x は、到達可能な最遠位置の範囲内であれば、開始点から数ジャンプ先の位置に到達できるため、最遠到達可能位置は x+nums[x] で更新できます。
走査プロセス中に、到達可能な最も遠い位置が配列内の最後の位置以上である場合、それは最後の位置が到達可能であることを意味し、True を直接返すことができます。走査後も最後の位置に到達できない場合は、False を返します。
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxIndex = 0;
for(int i = 0; i < nums.size(); ++i) {
if(i <= maxIndex) //i位置在最大范围内,说明i位置可以到达
{
maxIndex = max(maxIndex, i + nums[i]);//更新最大范围
if(maxIndex >= nums.size() - 1)
return true;//若最大范围包括最后一个位置,说明最后一个位置可以到达
}
}
return false;
}
};
6. コインチェンジ
難易度:簡単
貪欲なアルゴリズムのアイデアを使用して、各ステップでできるだけ多くの紙幣を使用します。日常生活でも同じことをしてください。値はプログラム内で昇順に並べられています
#include <iostream>
#include <vector>
using namespace std;
int Solve(int money, vector<pair<int, int>>& moneyCount)
{
int num = 0;
for (int i = moneyCount.size() - 1; i >= 0; --i)//逆序:先使用面值大的钱币
{
int count = min(moneyCount[i].second, money / moneyCount[i].first);//需要的数量和拥有的数量中选择较小的
money -= moneyCount[i].first * count;
num += count;
}
if (money != 0) return -1;//找不开钱
return num;
}
int main()
{
//first:面值 second:数量
vector<pair<int, int>> moneyCount = { {1,3},{2,1},{5,4},{10,3},{20,0},{50,1},{100,10} };
int money = 0;
cout << "请输入需要支付多少钱" << endl;
cin >> money;
int ret = Solve(money, moneyCount);
if (ret == -1) cout << "No" << endl;
else cout << ret << endl;
return 0;
}
7. 複数マシンのスケジューリング問題
難易度:中
この問題に対する有効な解決策 (最適解を求める) はありませんが、貪欲な選択戦略 (次善の解を求める) を使用して、より優れた近似アルゴリズムを設計できます。
n<=m の場合、各マシンにジョブを割り当てるだけで、n>m の場合、最初に n 個のジョブを最大から最小の順に並べ替えてから、アイドル状態のマシンにこの順序でジョブを割り当てます。つまり、すべてのジョブが処理されるか、マシンが他のジョブを処理できなくなるまで、残りのジョブから処理時間が最も長いジョブを選択し、次に処理時間が 2 番目に長いジョブを順番に選択します。最小の処理時間を必要とするジョブがアイドル状態のマシンに毎回割り当てられる場合、他のすべてのジョブが処理される可能性があり、最も長い時間がかかるジョブのみが処理される可能性があり、効率が低下します。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Compare {
public:
bool operator()(int x, int y) {
return x > y;
}
};
int GreedStrategy(vector<int>& works, vector<int>& machines)
{
//按作业时间从大到小排序
sort(works.begin(), works.end(), Compare());
//若作业数小于等于机器数,直接返回最大的作业时间即可
if (works.size() <= machines.size()) return works[0];
//从大到小为每个作业分配机器
for (int i = 0; i < works.size(); ++i) {
//假设选择第一个机器
int minMachines = 0;
int time = machines[minMachines];
//从机器中选择作业时间最小的
for (int j = 1; j < machines.size(); ++j) {
if (time > machines[j]) {
minMachines = j;
time = machines[j];
}
}
//将当前作业交给作业时间最小的机器
machines[minMachines] += works[i];
}
//从所有机器中选择总共作业时间最长的
int ret = machines[0];
for (int i = 1; i < machines.size(); ++i) {
ret = max(ret, machines[i]);
}
return ret;
}
int main()
{
int n = 0, m = 0;
cout << "请输入作业数和机器数" << endl;
cin >> n >> m;
vector<int> works(n);
vector<int> machines(m, 0);//存储每台机器总共的作业时间
cout << "请输入各个作业所需要的时间" << endl;
for (int i = 0; i < n; ++i) {
cin >> works[i];
}
cout << GreedStrategy(works, machines) << endl;
return 0;
}
8. アクティビティの選択
難易度:中
貪欲な戦略:
- 開始時刻が最も早いアクティビティを選択するたびに、最適解が得られない
- 継続時間の最も短いアクティビティを選択するたびに、最適解が得られない
- 終了時刻が最も早いアクティビティを選択するたびに、最適解が得られます。予定外の活動のためにできるだけ多くの時間を残すには、この方法を選択してください。次の図に示すアクティビティ セット S。アクティビティは、終了時間に従って単調に増加するように並べ替えられます。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Compare
{
public:
bool operator()(pair<int,int> p1, pair<int,int> p2) {
return p1.second < p2.second;
}
};
int GreedyActivitySelector(const vector<pair<int, int>>& act)
{
int num = 1;//记录可以举行的活动的个数
int i = 0;
for (int j = 1; j < act.size(); ++j) {
if (act[j].first >= act[i].second) {
i = j;
++num;
}
}
return num;
}
int main()
{
int number = 0;
cin >> number;
vector<pair<int, int>> act(number);
for (int i = 0; i < act.size(); ++i) {
cin >> act[i].first >> act[i].second;
}
//按照活动截止日期从小到大排序
sort(act.begin(), act.end(), Compare());
int ret = GreedyActivitySelector(act);
cout << ret << endl;
return 0;
}
9. 重複区間なし
難易度:中
方法1
この質問は、8 番目の質問「アクティビティの選択」と非常によく似ています。競合しない間隔の最大数を見つけ、間隔の総数から最大値を引いて、削除する間隔の最小数を取得します。
class Solution {
public:
static bool cmp(vector<int>& a, vector<int>& b) {
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
std::sort(intervals.begin(), intervals.end(), cmp);
int num = 1;
int i = 0;
for (int j = 1; j < intervals.size(); ++j) {
if (intervals[j][0] >= intervals[i][1]) {
i = j;
++num;
}
}
return intervals.size() - num;
}
};
方法 2
開始点に従って間隔を並べ替えると、貪欲なアルゴリズムが非常に良い役割を果たすことができます.
貪欲な戦略:
開始点の順序に従って間隔を考慮する場合. 前のポインタを使用して、最終リストに追加されたばかりの間隔を追跡します
ケース 1:
現在考慮されている 2 つの間隔は重複していません。
この場合、間隔を削除せず、次の間隔に prev を割り当てます。削除された間隔の数は変更されません。
ケース 2:
2 つの区間が重なり、後者の区間の終点が前の区間の終点より前にある。
この場合、単純に後者の間隔のみを使用できます。後者の間隔の長さは小さいため、より多くの間隔を収容するためにより多くのスペースを残すことができます。したがって、prev は現在の間隔 (削除された間隔の数 + 1) に更新されます。
ケース 3:
2 つの区間が重なり、後者の区間の終点が前の区間の終点の後にある
. この場合、貪欲な戦略を使用して問題に対処し、後者の区間を直接削除します。これにより、より多くの間隔に対応するためにより多くのスペースを残すこともできます。したがって、prev は変更されず、削除された間隔の数 + 1
bool cmp(const vector<int>& a, const vector<int>& b)
{
//按起点递增排序
return a[0] < b[0];
}
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return 0;
}
//按起点递增排序
sort(intervals.begin(), intervals.end(), cmp);
int end = intervals[0][0], prev = 0, count = 0;
for (int i = 1; i < intervals.size(); i++)
{
if (intervals[prev][1] > intervals[i][0]) {//两个区间冲突
if (intervals[prev][1] > intervals[i][1]) {
//情况2
prev = i;
}
//情况3
count++;
}
else {
//情况1
prev = i;
}
}
return count;
}
};