担当:zeke
第17回:ゲーム理論とか
京都大学工学部工業化学科B1
AtCoder:緑(1043)
競プロではC++を使っています
みんげーとかWebserviceとか
最近、自転車を買いました
今年からの初心者なのでお手柔らかにお願いします
ゲーム
解き方のアプローチがいっぱい!!!
有向グラフであることが分かります
Grundy数が0の状態を保持したとき必敗である
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 |
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 |
残された石0の時は自明
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 | 0 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
遷移先が1,2,3しかないのに注意
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 | 0 | 1 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 | 0 | 1 | 2 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 | 0 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
| 残された石 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Grundy数 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 | 0 |
残された石0の時は自明
遷移可能な状態で0以上の最小の整数
結論:石の数4の倍数で後手必勝
他は先手必勝
N 個の正整数 a1,a2,…,aNが与えられます。
K 個の石が山になっていて、先手と後手が交互に
山から a1,a2,…,aN のいずれかの個数の石をとる
を繰り返します。ただし、山に残っている石の個数より多くの個数の石をとることはできません。先に山から石がとれなくなった方の負け (最後の石をとった方が勝ち) です。先手と後手がお互いに最善を尽くしたとき、勝つのはどちらでしょう?
int main(){
//入力からのdpテーブル準備
//初期化dp[0]=0;
//どうでもいいですが、0index,1indexどっちがいいんだろ
rep3(i,1,k+1){
bool h=false;
rep(j,n){
int k=i-vec[j];
//一個でも負けパターンあったら、その状態は勝確
if(k>=0&&dp[k]==0)h=true;
}
//勝フラグを立てる
if(h)dp[i]=1;
}
if(dp[k])cout<<"First"<<endl;
else cout<<"Second"<<endl;
}Q をキューとする
dp テーブル全体を -1 に初期化する
for each 各ノード v do
if deg[v] == 0 then
dp[v] = 0
v を Q に push
while Q が空ではない do
Q からノード v を pop して取り出す
for each v を終点とする辺 e とその始点のノード nv do
if nv はすでに訪れたノード then
continue
辺 e をグラフから削除 (実装上は deg[nv] の値を減らせば OK)
if dp[v] = 0 then
dp[nv] = 1
nv を Q に push
else if dp[v] = 1 then
if deg[nv] = 0 then (辺を削除して行ったことで出次数が 0 になったら負けノード確定です)
dp[nv] = 0
nv を Q に pushQ をキューとする
dp テーブル全体を -1 に初期化する
for each 各ノード v do
if deg[v] == 0 then
dp[v] = 0
v を Q に push
while Q が空ではない do
Q からノード v を pop して取り出す
for each v を終点とする辺 e とその始点のノード nv do
if nv はすでに訪れたノード then
continue
辺 e をグラフから削除 (実装上は deg[nv] の値を減らせば OK)
if dp[v] = 0 then
dp[nv] = 1
nv を Q に push
else if dp[v] = 1 then
if deg[nv] = 0 then (辺を削除して行ったことで出次数が 0 になったら負けノード確定です)
dp[nv] = 0
nv を Q に push後退解析の例(drkenさんのサイトから)
負け確の頂点を決めるとき、出自数を事前にメモっておくと、決めやすくなります
コンテスト
個人的にVjudgeが嫌いなので、素敵なUIの
を使います!!!!!!!!!
誠に申し訳ありませんが、アカウント持っていない人は
ささっと作ってください
何と、自分の名前の色を自由に変えられます!
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
ll n,a,b;
cin>>n>>a>>b;
if(a>b){
cout<<"Takahashi"<<endl;
}else if(a==b){
if(n%(a+1)==0){
cout<<"Aoki"<<endl;
}else{
cout<<"Takahashi"<<endl;
}
}else{
if(n<=a){
cout<<"Takahashi"<<endl;
}else{
cout<<"Aoki"<<endl;
}
}
}int main() {
cin.tie(0);
ios::sync_with_stdio(false);
ll x,y;
cin>>x>>y;
if(abs(x-y)<=1){
cout<<"Brown"<<endl;
}else{
cout<<"Alice"<<endl;
}
}int main(){
int a,b;
cin>>a>>b;
if(a%2==0||b%2==0)cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}int main(){
int a,b;
cin >> a >> b;
vector<int> A(a+5);
vector<int> B(b+5);
//逆からやるのに留意
for(int i=0;i<a;i++){
int x;
cin >> x;
A[i+1]=x;
}
for(int i=0;i<b;i++){
int x;
cin >> x;
B[i+1]=x;
}
vector<vector<int>> dp(a+5,vector<int>(b+5));
//dp[i][j] 左にi右にj残っているときに先手が最善尽くしたときの価値の合計
//つまり先手は最善の手を、後攻は最悪手を放つように見える
for(int i=0;i<a+1;i++){
for(int j=0;j<b+1;j++){
int judge=(a+b-i-j)%2;
//先手か後手か?
if(i==0 && j==0){
dp[i][j]=0;
//なんもないときは当然0
//山がどちらかないとき
}else if(i==0){
if(judge==1){
dp[i][j]=dp[i][j-1];
//後手がとっても先手の合計は増えないことに注意
}else{
dp[i][j]=dp[i][j-1]+B[b+1-j];
}
}else if(j==0){
if(judge==1){
dp[i][j]=dp[i-1][j];
}else{
dp[i][j]=dp[i-1][j]+A[a+1-i];
}
}else{
if(judge==1){
dp[i][j]=min(dp[i-1][j],dp[i][j-1]);
//もちろんその場での最悪手をとる
}else{
dp[i][j]=max(dp[i-1][j]+A[a+1-i],dp[i][j-1]+B[b+1-j]);
//最善手
}
}
}
}
cout<<dp[a][b]<<endl;
}int main() {
cin.tie(0);
ios::sync_with_stdio(false);
int h,w;
cin>>h>>w;
if(h==1&&w==1){
cout<<"First"<<endl;
return 0;
}
if(h==1){
//高さ1の時、例外処理
rep(i,w){
char s;
cin>>s;
if(s=='#'){
if(i%2==0){
cout<<"First"<<endl;
}else{
cout<<"Second"<<endl;
}
return 0;
}
}
if(w%2==0){
cout<<"First"<<endl;
}else{
cout<<"Second"<<endl;
}
return 0;
}
if(w==1){
//幅1の時、例外処理
rep(i,h){
char s;
cin>>s;
if(s=='#'){
if(i%2==0){
cout<<"First"<<endl;
}else{
cout<<"Second"<<endl;
}
return 0;
}
}
if(h%2==0){
cout<<"First"<<endl;
}else{
cout<<"Second"<<endl;
}
return 0;
}
VV vec(h,V(w));//二次元テーブル
rep(i,h){
string s;
cin>>s;
rep(j,w){
if(s[j]=='#'){
vec[i][j]=-2;//壁には-2を入れておく
}
}
}
if(vec[h-1][w-1]==0){
vec[h-1][w-1]=-1;//負けマスには0を入れておきます
}
//以下、端っこで詰むところをメモします
for(int i=h-2;i>=0;i--){
if((vec[i+1][w-1]==-2||vec[i+1][w-1]==1)&&vec[i][w-1]==0){
vec[i][w-1]=-1;
}else if(vec[i+1][w-1]==-1&&vec[i][w-1]==0){
vec[i][w-1]=1;
}
}
for(int i=w-2;i>=0;i--){
if((vec[h-1][i+1]==-2||vec[h-1][i+1]==1)&&vec[h-1][i]==0){
vec[h-1][i]=-1;
}else if(vec[h-1][i+1]==-1&&vec[h-1][i]==0){
vec[h-1][i]=1;
}
}
//やっとDPです、勝ちマスには1を入れます
for(int i=h-2;i>=0;i--){
for(int j=w-2;j>=0;j--){
if(vec[i][j]!=0)continue;
if(vec[i+1][j]==-1||vec[i+1][j+1]==-1||vec[i][j+1]==-1){
vec[i][j]=1;
}else{
vec[i][j]=-1;
}
}
}
if(vec[0][0]==1){
cout<<"First" <<endl;
}else{
cout<<"Second" <<endl;
}
}int main(){
ll n,z,w;
cin>>n>>z>>w;
V vec(n);
rep(i,n)cin>>vec[i];
ll res=abs(vec[n-1]-w);
if(n>=2)chmax(res,abs(vec[n-1]-vec[n-2]));
cout<<res<<endl;
}#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// グラフの頂点数、辺数、各ノードの重み
int N, M;
vector<int> X;
// ゲームグラフ (後退解析のために辺の向きを逆にする)
vector<vector<int> > G;
vector<int> deg; // ゲームグラフにおける出次数 (辺の向きを逆にした状態で入次数)
// 二分探索判定 (D 以上にできるかどうか)
bool judge(int D) {
vector<int> cdeg = deg;//この場限りの出次数
queue<int> que; // キュー
vector<int> dp(N * 2, -1); // 最初は全体を -1 で初期化
// 初期条件、分かった頂点からqueueに突っ込んで伸ばしていくぞい
for (int i = 0; i < N; ++i) {
if (X[i] >= D) {
dp[i] = 1; // 先手勝ち
que.push(i);
if (cdeg[i] == 0) {
dp[i + N] = 0; // 後手負け これ以上動けないため
que.push(i + N);
}
}
else {
dp[i + N] = 1; // 後手勝ち
que.push(i + N);
if (cdeg[i] == 0) {
dp[i] = 0; // 先手負け
que.push(i);
}
}
}
// 後退解析 (ここは自動的に書ける)
while (!que.empty()) {
int v = que.front(); que.pop();//幅優先と同じ要領
for (auto nv : G[v]) {
// すでに見た頂点はスルー
if (dp[nv] != -1) continue;
--cdeg[nv]; // 辺 (nv, v) を削除
if (dp[v] == 0) {//遷移先が相手にとって負け確だったら
dp[nv] = 1;//今いるところは勝確!
que.push(nv);//その状態から、さらに前を見るぞ
}
else if (dp[v] == 1) {//相手が勝ちの時…
if (cdeg[nv] == 0) {
//もし相手の勝ち状態にしか遷移できなかったら…
dp[nv] = 0;//その状態は負け!
que.push(nv);//その状態から、さらに前を見るぞ
}
}
}
}
// ノード 0 が先手番で勝ちかどうか
return (dp[0] == 1);
}
//状態数は頂点数*2(先手と後手)
int main() {
// 入力
cin >> N >> M;
X.resize(N*2);
for (int i = 0; i < N; ++i) cin >> X[i];
for (int i = 0; i < N; ++i) X[i+N] = X[i];
G.assign(N*2, vector<int>());
deg.assign(N*2, 0);
for (int i = 0; i < M; ++i) {
int a, b; cin >> a >> b; --a, --b;
G[b + N].push_back(a); // 後手番での後退遷移
G[b].push_back(a + N); // 先手番での後退遷移
deg[a]++; deg[a + N]++;
}
// 二分探索
int low = 0, high = 1000000007; // low: 絶対先手勝つ、high: 絶対先手負ける
while (high - low > 1) {
int mid = (low + high) / 2;
if (!judge(mid)) high = mid;
else low = mid;
}
cout << low << endl;
}