非対称DP

目次

1つ、対称DP、非対称DP

1.対称DP

2.非対称DP

第二に、非対称DP​​の実際のOJ戦闘

CSU 1207:厳密に増加するシーケンス

HDU1176無料パイ

POJ-1390ブロック

POJ3661実行中


1つ、対称DP、非対称DP

これは、DPの理解と導入に基づいて私が提唱した新しい概念です。

1.対称DP

シーケンスDPhttps  ://blog.csdn.net/nameofcsdn/article/details/112771904

インターバルDPhttps  ://blog.csdn.net/nameofcsdn/article/details/112981922

オブジェクトスペースはソリューションスペースhttps://blog.csdn.net/nameofcsdn/article/details/111885131と等しいため、これら2つのタイプのDPは実際には比較的単純です 

問題にf(i、j)が必要な場合、アルゴリズムの漸化式はf(i、j)= .....になります。

ここでのiとjは同じ概念であるため、ステータスは対称であり、オブジェクト空間とソリューションスペースのステータスは対称であるため、対称DPと呼びます。

2.非対称DP

DPの問題がいくつかあります。たとえば、問題はf(n)に抽象化されます。fの再発は非常に複雑で、存在しない場合もあります。

しかし、補助概念を導入し、対応する補助空間で変数iを取り、サブ問題g(n、i)を取得することができます。

g(n、i)の漸化式に従ってその値を計算し、最後にf(n)= h({g(n、i)|i∈A})に従ってf(n)を計算します

その中で、hはサブ問題、一般的には和関数または最大関数または最小関数の解の積分に従って元の問題の解を取得する方法を表し、Aは補助空間を表します

nが配置されているオブジェクト空間と補助空間Aが一緒になって解空間を構成する、つまりオブジェクト空間と解空間が異なるため、nとiは同じ概念ではないため、非対称DP​​と呼びます。

 

第二に、非対称DP​​の実際のOJ戦闘

CSU 1207:厳密に増加するシーケンス

トピック:

説明

シーケンス内のいずれかのアイテムが前のアイテムよりも大きい場合、このシーケンスを厳密に増加するシーケンスと呼びます。

これで整数のシーケンスができました。シーケンス内のいくつかの隣接するアイテムを1つのアイテムにマージできます。マージ後、このアイテムの値は、マージ前のアイテムの値の合計になります。数回マージした後、最終的に厳密に増加するシーケンスを取得する必要があるので、取得した厳密に増加するシーケンスは最大でいくつのアイテムを持つことができますか?

入力

入力データの最初の行には、正の整数T(1 <= T <= 200)が含まれており、テストデータの合計Tグループが存在することを示しています。

テストデータの各グループの最初の行には整数N(1 <= N <= 1000)が含まれており、この整数シーケンスにN個の項目があることを示しています。次の行には、10 ^ 6以下のN個の正の整数が含まれており、このシーケンスの各項目の値を順番に記述しています。

最大20セットのデータがN> 100を満たします。

出力

テストデータのセットごとに、1行を使用して整数を出力します。これは、最終的に厳密に増加するシーケンスの項目の最大数を示します。

サンプル入力

3
2
1 1
3
1 2 3
5
1 3 2 6 7

サンプル出力

1 
3 
4

アイデア:インターバルDP

dp [i] [j]は、1からjまでのjの数と、最後のセグメントがiからjまでの場合の最適解の値(0の場合もあります)を表します。

コード:

#include<iostream>
using namespace std;
 
int dp[1001][1001];//dp[i][j]表示最后一段为i到j的最优解
 
int main()
{
	int T, n, num[1001], sum[1001];
	cin >> T;
	while (T--)
	{
		cin >> n;
		sum[0] = 0;
		for (int i = 1; i <= n; i++)
		{
			cin >> num[i];
			dp[1][i] = 1, dp[i][i - 1] = 0;
			sum[i] = sum[i - 1] + num[i];
		}
		for (int i = 1; i < n; i++)
		{
			int ki = i;
			for (int j = i + 1; j <= n; j++)
			{
				dp[i + 1][j] = dp[i + 1][j - 1];
				while (ki)
				{
					if (sum[i] - sum[ki-1] >= sum[j] - sum[i])break;
					if (dp[ki][i] > 0 && dp[i + 1][j] < dp[ki][i] + 1)
						dp[i + 1][j] = dp[ki][i] + 1;
					ki--;
				}
			}
		}
		int ans = 0;
		for (int i = 1; i <= n; i++)if (ans < dp[i][n])ans = dp[i][n];
		cout << ans << endl;
	}
	return 0;
}

HDU1176無料パイ

トピック:

説明

空にはパイがないと言われていますが、ある日、ゲームボーイが帰り道を歩いていたところ、突然たくさんのパイが空から落ちてきました。このゲームボーイのキャラクターは本当に良いです、このパイは他のどこにも落ちませんでした、それは彼の側から10メートル以内に落ちました。パイが地面に落ちた場合はもちろん食べられないので、ゲームボーイはすぐにバックパックを外して拾いました。しかし、トレイルの両側に誰もいないので、彼はトレイルでしか拾うことができません。ゲームボーイは通常、ゲームをプレイするために部屋にとどまるため、ゲームでは非常に機敏なプレーヤーですが、実際には運動神経が特に遅く、毎秒1メートル以内の範囲内で落下するパイしか捕まえられません。アイコンに示されているように、このトレイルの座標を指定します。 
問題を単純化するために、次の期間にパイが0から10の11の位置に落ちると仮定します。当初、ゲームボーイはポジション5に立っていたため、最初の1秒間は、3つのポジション4、5、6のいずれかでしかパイを受け取ることができませんでした。ゲームボーイは最大でいくつのパイを受け取ることができますか?(彼のバックパックが無限の数のパイを保持できると仮定して) 

入力

入力データには複数のグループがあります。データの各グループの最初の行は正の整数n(0 <n <100000)です。これは、n個のパイがこのパスに落ちたことを意味します。結果のn行では、各行に2つの整数x、T(0 <T <100000)があります。これは、パイが2番目のTで点xにあることを意味します。複数のパイが同じ秒の同じポイントに落ちる可能性があります。n = 0の場合、入力は終了します。 

出力

入力データの各セットは、1行の出力に対応します。整数mを出力します。これは、ゲームボーイが最大m個のパイを受け取る可能性があることを示します。 
ヒント:この質問の入力データの量は比較的多いです。読み取りにはscanfを使用することをお勧めします。cinを使用するとタイムアウトになる場合があります。 

サンプル入力

6
5 1
4 1
6 1
7 2
7 2
8 3
0

サンプル出力

4

この問題は明らかに動的計画法であり、11配列のDP問題として理解できます。これは、数学的帰納法のスパイラル帰納法の拡張バージョンに相当します。

コード:

#include<iostream>
#include<stdio.h>
using namespace std;
 
 
int n, x, t;
int num[100001][11];
 
int get(int i, int j)
{
	int r = num[i - 1][j];
	if (j && r < num[i - 1][j - 1])r = num[i - 1][j - 1];
	if (j < 10 && r < num[i - 1][j + 1])r = num[i - 1][j + 1];
	return r;
}
 
int main()
{
	while (scanf("%d", &n))
	{
		if (n == 0)break;
		for (int j = 0; j <= 100000; j++)
			for (int i = 0; i < 11; i++)num[j][i] = 0;
		for (int i = 1; i <= n; i++)
		{
			scanf("%d%d", &x, &t);
			if (x >= 5 - t && x <= 5 + t)num[t][x] ++;
		}
		for (int j = 1; j <= 100000; j++)
			for (int i = 0; i < 11; i++)num[j][i] += get(j, i);		
		int maxx = 0;
		for (int i = 0; i < 11; i++)
			if (maxx < num[100000][i])maxx = num[100000][i];
		printf("%d\n", maxx);
	}
	return 0;
}

入力が何であっても、プログラムをソートする必要はありません。

このコードは、時間を最適化し続けることもできます。多くの場所で、100,000は必要ありません。入力データに応じて、必要な大きさを決定します。

しかし、これはわずか92ms ACであり、変更するには怠惰です。

入力されたn個のパイの1つが3,1の場合、パイは受信されないことに注意してください。

この状況は区別する必要があり、それに対処する方法は、数えずに単に無視することです。

判定条件はx> = 5-t && x <= 5 + tです。これを満たすものはすべて受け取ることができるパイであり、満たされないものはそうではありません。

POJ-1390ブロック

トピック:

「ブロック」と呼ばれるゲームをプレイしたことがある方もいらっしゃるかもしれません。行にはn個のブロックがあり、各ボックスには色があります。次に例を示します:ゴールド、シルバー、シルバー、シルバー、シルバー、ブロンズ、ブロンズ、ブロンズ、ゴールド。 
対応する画像は次のようになります。 
隣接するボックスがすべて同じ色で、左側のボックス(存在する場合)と右側のボックス(存在する場合)の両方が別の色である場合、これを「ボックスセグメント」。4つのボックスセグメントがあります。つまり、金、銀、青銅、金です。セグメントにはそれぞれ1、4、3、1個のボックスがあります。 
毎回、ボックスをクリックすると、そのボックスを含むセグメント全体が消えます。そのセグメントがk個のボックスで構成されている場合、k * kポイントを取得します。たとえば、銀色のボックスをクリックすると、銀色のセグメントが消え、4 * 4 = 16ポイントを獲得します。 
下の写真を見てみましょう
。最初の写真 はOPTIMALです。 
このゲームの初期状態を考慮して、取得できる最高のスコアを見つけます。 

入力

最初の行には、テストの数t(1 <= t <= 15)が含まれています。各ケースには2行が含まれています。最初の行には、ボックスの数である整数n(1 <= n <= 200)が含まれています。2行目には、各ボックスの色を表すn個の整数が含まれています。整数は1〜nの範囲です。

出力

テストケースごとに、ケース番号と可能な限り高いスコアを印刷します。

サンプル入力

2 
9 
1 2 2 2 2 3 3 3 1 
1 
1

サンプル出力

ケース1:29
ケース2:1

コード:

#include<iostream>
using namespace std;
 
int num[201], col[201], ans[201][201][201];//前面附加t个同色盒子
 
int dp(int l, int r,int t)
{
	if (l > r)return 0;
	if (l == r)return (num[l] + t) * (num[l] + t);
	if (ans[l][r][t])return ans[l][r][t];
	int a = dp(l, l, t) + dp(l + 1, r, 0);
	for (int i = l + 1; i <= r; i++)
	{
		if (col[i] != col[l])continue;
		if (a < dp(l + 1, i - 1, 0) + dp(i, r, t + num[l]))
			a = dp(l + 1, i - 1, 0) + dp(i, r, t + num[l]);
	}
	ans[l][r][t] = a;
	return a;
}
int main()
{
	int T, n;
	cin >> T;
	for(int i=1;i<=T;i++)
	{
		cin >> n;
		int c1 = -1, c2, key = 0;
		for (int i = 0; i <= n; i++)
		{
			num[i] = 0;
			for (int j = 0; j <= n; j++)for(int t=0;t<=n;t++)ans[i][j][t] = 0;
		}
		for (int i = 0; i < n; i++)
		{
			cin >> c2;
			if (c1 != c2)c1 = c2, col[++key] = c2;
			num[key]++;
		}
		cout << "Case " << i << ": " << dp(1, key, 0) << endl;
	}
	return 0;
}

POJ3661実行中

トピック:

説明

牛はベッシーが正確にするためにトラック上で実行されているので、より良い選手になることをしようとしている N  (1≤  N  ≤10,000)分。1分間ごとに、彼女は1分間走るか休むかを選択できます。

究極の距離ベッシーの実行は、しかし、彼女は分で実行することを選択した場合は0から始まり、彼女の「消耗率」に依存して 私を彼女はまさにの距離実行され、 ディ (1≤ ディ ≤千)と彼女の消耗率意志を1ずつ増加-しかし、超えてはなりません M  (1≤  M  ≤500)。彼女が休むことを選択した場合、彼女の倦怠感は彼女が休む毎分1ずつ減少します。倦怠感が0になるまで、彼女は再び走り始めることはできません。その時点で、彼女は走るか休むかを選ぶことができます。

N 分間のトレーニングの終わりに 、ベッシーの発疹係数は正確に0でなければなりません。そうでないと、彼女はその日の残りの時間に十分なエネルギーが残っていません。

Bessieが走ることができる最大距離を見つけます。

入力

* 1行目:2つのスペースで区切られた整数:  N と M
* 2行目。N + 1:i +1行 目には1つの整数が含まれています:  Di

出力

* 1行目:Bessieが条件を満たすときに実行できる最大距離を表す単一の整数。

サンプル入力

5 2
5
3
4
2
10

サンプル出力

9

この問題のために、私は最初から最後まで4つのアイデアを生み出しました。最初の3つのアイデアはすべて死に、最後の1つはついに成功しました。

アイデア1、スーパーメモリ+タイムアウト、ACなし、コードは正しいが、効率は非常に低い。

2番目のアイデアは、質問の制限条件を満たせないため、コードが間違っているということです。

アイデア3、時間の経過とともに、コードは正しいですが、効率は十分に高くありません。

4番目のアイデアは完全に正しいです。

 

アイデア1:時間を多くのセグメントに分割します。各セグメントは、発疹係数0で始まり、発疹係数0で終わり、実行できる最大距離を見つけます。

sum [i] [j]は、上記の条件を満たすためのiからjまでの最大距離を表します。

コード:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][10001];
int a;
int n, m;
 
int f(int i, int j)		
{
	if (sum[i][j] >= 0)return sum[i][j];
	if (i >= j)return 0;
	if (i + 1 == j)return list[i];
	if (j - i + 1 <= m * 2 && (j - i) % 2)
	{
		sum[i][j] = 0;
		for (int k = i; k <= (i + j) / 2; k++)sum[i][j] += list[k];
	}
	for (int k = i; k < j; k++)
	{
		a = f(i, k) + f(k + 1, j);
		if (sum[i][j] < a)sum[i][j] = a;
	}
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(1, n);
	return 0;
}

送信したところ、10,000 x 10,000の配列が大きすぎて、メモリが多すぎることがわかりました。

実際、これは単なるメモリの問題ではありません。このアイデアの時間計算量は非常に高く、望ましくありません。

 

アイデア2:i番目の分の終わりを見つけて、発散係数がjの場合、最大距離を実行できます。

sum [i] [j]は、iがnを超えず、jがmを超えず、i番目の分の終了疲労係数がjの場合に実行できる最大距離を表します。

なぜ私は恣意的で深遠な意味を強調したいのですか、読んでください(アイデア3の4行目)

自分で定義した状態和配列を曖昧さなく明確にするだけで、間違いを犯しにくくなります。

コード:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][501];
int a,b;
int n, m;
 
int f(int i, int j)		
{
	if (sum[i][j] >= 0)return sum[i][j];
	if (i == 1)
	{
		if (j)return list[1];
		return 0;
	}
	if (j > 0)sum[i][j] = f(i - 1, j - 1) + list[i];
	if (j < m)
	{
		a = f(i - 1, j + 1);
		if (sum[i][j] < a)sum[i][j] = a;
		if (j == 0)
		{
			a = f(i - 1, 0);
			if (sum[i][j] < a)sum[i][j] = a;
		}
	}
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(n, 0);
	return 0;
}

ここで私が確信していない詳細が1つあります。つまり、sum [1] [j]で、jが1より大きい場合、それは理にかなっています。

sum [1] [j]のjが1より大きいことは非現実的だと思いますが、問題の最適解には影響しないはずですが、よくわかりません。

このコードは、問題の制限を保証するものではありません。休憩を開始したら、実行を継続するには、発疹係数が0になるまで休憩し、断固としてあきらめる必要があります。

 

アイデア3:sum [i] [j]は、i番目の分の終わりを意味します。発火係数がjの場合、最大距離を実行できます。

ただし、制限があります。jが0でない場合、時間の関数としての拡張係数の最大点を表します。

つまり、f(6,3)は、4分、5分、6分で走っている場合にどれだけ走れるかを示し、走った後の発汗係数は0です。

このような状態は、実行中に生成される可能性のある状態の合計を定義する代わりに、非常に興味深いものです。

合計は、時間変化に関する拡張係数の関数の最大点とゼロ点でのみ定義されます。

一部のsum [i] [j]は計算されませんが、定義されていないことに注意してください。前者は計算方法のわずかな違いであり、後者は考え方の本質的な違いです!

コード:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][501];
int a;
int n, m;
 
int f(int i, int j)
{
	if (i <= 0)return 0;
	if (sum[i][j] >= 0)return sum[i][j];	
	if (j == 0)
	{
		sum[i][j] = f(i - 1, 0);		//第i分钟在休息
		for (int k = i-m; k < i; k++)
		{
			a = f(k, i - k);
			//第k分钟在跑,到了顶点,然后开始休息,直到第i分钟结束才让疲劳度降为0
			if (sum[i][j] < a)sum[i][j] = a;
		}
	}
	else
	{
		a = 0;		//j不为0那么就是疲劳度的极大值点
		int ki = i;	//不需要进行任何的分类讨论,也不需要max函数之类的
		for (int kj = j; ki>0 && kj>0; ki--,kj--)a += list[ki];
		sum[i][j] = a + f(ki, 0);
	}
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(n, 0);
	return 0;
}

このコードはまだタイムアウトします。

主にf関数で慎重に考えてください。jが0でない場合、ループが発生します。

議論の必要はありませんが、ループは非常に短いようですが、fを絶えず呼び出す場合、このループの作業は大幅に繰り返されます。

たとえば、ランニングプラン1、1〜20分のランニング、21〜40分の休憩、

ランニングプラン2、1〜19分のランニング、20〜40分の休憩。

明らかに、プログラム1はプログラム2よりも優れています。この比較について考えたところ、実際には、これら2つのプログラムの類似性が高いことがわかりました。

さらに2つの任意のスキームでは、このような感覚はありません。

上記のコードを見て、sum [20] [20]を計算し、sum [19] [19]を計算すると、何が見つかりましたか?

おやおや!ほとんどすべての作業(つまり、加算操作)が繰り返されます!

問題の鍵を理解して、私たちは答えからそう遠くはありません。

 

アイデア4:sum [i] [j]は、i番目の分の終わりを意味します。言い訳係数がjの場合、最大距離を実行できます。

ただし、制限があります。jが0でない場合は、i番目の分に実行できる最大距離を意味します。

アイデア3を比較すると、アイデア3は、疲労関数の最大点とゼロ点での合計のみを定義していることがわかります。これらは非常にまばらです。

4番目のアイデアは、jが0であるか、i番目の分が実行されている合計を定義することです。

(ちなみに、このように定義された合計は、実際には定義の半分にすぎません。これは、1分間のランニングの倦怠感が1増加し、1分間の休息の倦怠感が1減少するためです。

疲労関数は、多くの対称的なピークで構成されています。凸状ピーク間の距離は非常に大きくなる可能性がありますが、動的計画法アルゴリズムでは、

最適なソリューションが必要なため、実際には、ピーク間の距離は1以下です。それ以外の場合は、1分間実行し、途中で1分間休憩して、小さなピークを形成できます)

もちろん、これは重要な発見ではありません。重要なのは、3番目のアイデアで述べた二重計算が4番目のアイデアで解決できることです。

コード:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][501];
int a;
int n, m;
 
int f(int i, int j)
{
	if (i <= 0)return 0;
	if (sum[i][j] >= 0)return sum[i][j];	
	if (j == 0)
	{
		sum[i][j] = f(i - 1, 0);
		for (int k = i-m; k < i; k++)
		{
			a = f(k, i - k);
			if (sum[i][j] < a)sum[i][j] = a;
		}
	}
	else sum[i][j] = f(i - 1, j - 1) + list[i];
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(n, 0);
	return 0;
}

アイデア4のコードはアイデア3とほぼ同じです。唯一の違いは、f関数では、jが0でない場合、実際には、単純なステートメントのみがsum [i] [j]を計算できることです。

他に変更する必要はありません。

よく考えてみると、3つ考えと4つ考えの両方で、2つ考えの抜け穴に対処できることがわかります。問題の制限を満たすには、倦怠感が0になるまで休んでから実行を続ける必要があります。

しかし、方法はまったく異なります。

3番目のアイデアは、頂点(jが0ではない)で合計を計算するときに、合計を直接使用して、実行の開始(疲労度が0)に進むことです。

4番目のアイデアは、jが0である合計を求めるときに、残りの先頭に直接進むことです。

jが0の合計を求める場合、2つのアイデアのコードはまったく同じですが、アイデアが異なり、アイデアが異なるため、制限を満たすために選択された方法が異なると、効率も異なります。

それは私がACできる最も難しいDPの問題です。

おすすめ

転載: blog.csdn.net/nameofcsdn/article/details/113064148