COCI2011/2012 #1 skakac 状态压缩加速DP

COCI2011/2012 #1 skakac 状态压缩加速DP

题目链接:在里面找一下


? 写在前面:原题的空间为 64MB。但本题在校内 OJ 上空间开了 128MB。而本题解介绍的写法在 64MB 的限制下无法通过。若要在 64MB 的空间内实现,需要根号分治。


? 这题有一个比较显然的暴力,开一个三维数组 \(dp_{k,i,j}\) 表示第 \(k\) 个时刻,\((i,j)\) 这个位置是否能够到达。但是这个暴力时空的复杂度都为 \(O(T\times n^2)\)。我们接受不了。显然,第一维可以滚动掉,这样我们的空间就够了,但是时间还不够。注意到,\(dp\) 数组记录的是可行性,而 \(n\) 的大小很小,只有 \(30\)。我们可以考虑压位,即用一个二进制数来表示一行的可行性状态(bitset 貌似是这么实现的)。此时如果没有限制的话,我们可以很轻松的写出复杂度为 \(O(T\times n)\) 的转移。

? 代码如下:

for(R int k=1;k<=T;++k)
	{
		g^=1;e=g^1;
		memset(dp[g],0,sizeof dp[g]);
		for(R int i=1;i<=n;++i)
		{
			if(i>1)
			{
				dp[g][i]|=dp[e][i-1]<<2;
				dp[g][i]|=dp[e][i-1]>>2;
			}
			if(i>2)
			{
				dp[g][i]|=dp[e][i-2]<<1;
				dp[g][i]|=dp[e][i-2]>>1;
			}
			if(i<n)
			{
				dp[g][i]|=dp[e][i+1]<<2;
				dp[g][i]|=dp[e][i+1]>>2;
			}
			if(i<n-1)
			{
				dp[g][i]|=dp[e][i+2]<<1;
				dp[g][i]|=dp[e][i+2]>>1;
			}
		}
	}

? 但是,题目是有限制的,有些格子只有在特定时间的时候才能跳到。所以我们可以再开一个数组 \(can_{i,j}\) 表示第 \(i\) 个时刻第 \(j\) 行格子可行位置的状态。那么 \(dp_{i,j} \& can_{i,j}\) 得到的就是 \(i\) 这个时刻第 \(j\) 行能够到达的状态

? \(can\) 数组可以通过预处理得到。代码如下:

for(R int i=1;i<=n;++i)
	{
		for(R int j=1;j<=n;++j)
		{
			scanf("%d",&a[i][j]);
			for(R int k=a[i][j];k<=T;k+=a[i][j]) can[k][i]|=1<<(j-1);
		}
	}

? 但是这样预处理的时间复杂度上界依然是 \(O(T\times n^2)\)。考虑到时间主要是因为 \(a_{i,j}\) 过小而浪费的,我们可以定义一个参数 \(P\),如果 \(a_{i,j}\le P\) ,我们不预处理它们,而是直接记录,然后在转移的时候,枚举 \(1\)\(P\) 计算出格子中的值为在 \([1,P]\) 范围内的能否到达的情况。那么时间复杂度就为 \(O(\dfrac{T}{P+1}+T\times P+T\times n)\)。对于参数 \(P\) 自己算一下,微调一下就好了。

? 那么预处理和转移的代码就变成如下:

for(R int i=1;i<=n;++i)
	{
		for(R int j=1;j<=n;++j)
		{
			scanf("%d",&a[i][j]);
			if(a[i][j]<=P) d[a[i][j]][i]|=1<<(j-1);
			else for(R int k=a[i][j];k<=T;k+=a[i][j]) can[k][i]|=1<<(j-1);
		}
	}
	int g=0,e=0;
	for(R int k=1;k<=T;++k)
	{
		g^=1;e=g^1;
		int tmp[31]={};
		memset(dp[g],0,sizeof dp[g]);
		for(R int j=1;j<=P;++j) if(k%j==0) for(R int i=1;i<=n;++i) tmp[i]|=d[j][i];
		for(R int i=1;i<=n;++i)
		{
			if(i>1)
			{
				dp[g][i]|=dp[e][i-1]<<2;
				dp[g][i]|=dp[e][i-1]>>2;
			}
			if(i>2)
			{
				dp[g][i]|=dp[e][i-2]<<1;
				dp[g][i]|=dp[e][i-2]>>1;
			}
			if(i<n)
			{
				dp[g][i]|=dp[e][i+1]<<2;
				dp[g][i]|=dp[e][i+1]>>2;
			}
			if(i<n-1)
			{
				dp[g][i]|=dp[e][i+2]<<1;
				dp[g][i]|=dp[e][i+2]>>1;
			}
			dp[g][i]&=(tmp[i]|can[k][i]);
		}
	}

? 整个代码的空间瓶颈是 \(can\) 数组。所以本文的代码无法在 64MB 空间限制的情况下通过。

全部代码如下:

#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define R register
using namespace std;
const int MAXN = 1e6+5;
const int P = 7;
int n,T,x,y;
int dp[2][31],a[35][35],can[MAXN][31],d[10][31];
int main()
{
	scanf("%d %d",&n,&T);
	scanf("%d %d",&x,&y);
	dp[0][x]|=1<<(y-1);
	for(R int i=1;i<=n;++i)
	{
		for(R int j=1;j<=n;++j)
		{
			scanf("%d",&a[i][j]);
			if(a[i][j]<=P) d[a[i][j]][i]|=1<<(j-1);
			else for(R int k=a[i][j];k<=T;k+=a[i][j]) can[k][i]|=1<<(j-1);
		}
	}
	int g=0,e=0;
	for(R int k=1;k<=T;++k)
	{
		g^=1;e=g^1;
		int tmp[31]={};
		memset(dp[g],0,sizeof dp[g]);
		for(R int j=1;j<=P;++j) if(k%j==0) for(R int i=1;i<=n;++i) tmp[i]|=d[j][i];
		for(R int i=1;i<=n;++i)
		{
			if(i>1)
			{
				dp[g][i]|=dp[e][i-1]<<2;
				dp[g][i]|=dp[e][i-1]>>2;
			}
			if(i>2)
			{
				dp[g][i]|=dp[e][i-2]<<1;
				dp[g][i]|=dp[e][i-2]>>1;
			}
			if(i<n)
			{
				dp[g][i]|=dp[e][i+1]<<2;
				dp[g][i]|=dp[e][i+1]>>2;
			}
			if(i<n-1)
			{
				dp[g][i]|=dp[e][i+2]<<1;
				dp[g][i]|=dp[e][i+2]>>1;
			}
			dp[g][i]&=(tmp[i]|can[k][i]);
		}
	}
	int ans=0;
	for(R int i=1;i<=n;++i)
		for(R int j=1;j<=n;++j) if(dp[T&1][i]&(1<<(j-1))) ++ans;
	printf("%d\n",ans);
	for(R int i=1;i<=n;++i)
		for(R int j=1;j<=n;++j) if(dp[T&1][i]&(1<<(j-1))) printf("%d %d\n",i,j);
	return 0;
}

? 关于 64MB 限制的做法,我粗略的看了下官方题解。大致是对于大于 \(1000\)\(a\) 值,我们直接枚举它的倍数,对于小于 \(1000\)\(a\) 值,我们对其进行质因数分解,然后以更小的空间处理出类似本文 \(can\) 数组的数组。

? 因为懒,就贴份官方代码好了。。。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

int dx[] = { -2, -2, -1, -1, 1, 1, 2, 2 };
int dy[] = { -1, 1, -2, 2, -2, 2, -1, 1 };

const int MAXN = 31;
const int MAXT = 1000001;
const int MAXP = 170; //prosti brojevi manji od 1000
const int MAXPOT = 20; //najveca potencija nekog prostog broja u rastavu broja t

int pr[MAXT], f[MAXT], ff[MAXT], fp[MAXT];
int k[MAXN][MAXN], b[MAXN], bb[MAXN], m[MAXN];
int n, T;

int ind[1000], prosti[MAXP], cnt;
int faktor[MAXP][MAXPOT][MAXN]; // matrica F(p na q)
int fnula[MAXP][MAXP][MAXN]; // matrica G(x,y)

vector< int > v[MAXT];

inline void postavi( int &x, int y, int v ) { // postavlja y-ti bit u x na vrijednost v
  if( v == 1 ) x |= (1<<y); else
    x &= ~(1<<y);
}

inline int daj( int x, int y ) { return (x>>y)&1; } // vraca vrijednost y-tog bita u x

void sito( int MAX ) {
// izracunava sve proste brojeve manje od 1000
   for( int i = 2; i*i < MAXT; ++i )
    if( !pr[i] ) {
      for( int j = i+i; j < MAXT; j += i )
        pr[j] = 1, f[j] = i;
      ind[i] = cnt;
      prosti[cnt++] = i;
    }

// izracunava po jedan prosti faktor i njegov eksponent za svaki broj manji od MAXT kako bi mogli brojeve rastavljati na proste faktore
    for( int i = 2; i < MAXT; ++i ) {
        if( f[i] == 0 ) f[i] = i;
        int x = i;
        ff[i] = 1, fp[i] = 0;
        while( x%f[i] == 0 ) x /= f[i], ff[i] *= f[i], fp[i]++;
    }
}

void solveG() { // izracunava funkciju G(i,j)
    for( int i = 0; i < cnt; ++i ) {
        for( int j = 0; j < n; ++j )
            fnula[i][i][j] = faktor[i][0][j];

    for( int j = i+1; j < cnt; ++j )
      for( int k = 0; k < n; ++k )
        fnula[i][j][k] = (fnula[i][j-1][k] & faktor[j][0][k] );
  }
}

int main( void ) {
  scanf( "%d %d", &n, &T );

  int px, py;
  scanf( "%d %d", &px, &py ); --px, --py;

  sito( MAXT );

  for( int i = 0; i < n; ++i )
    for( int j = 0; j < n; ++j ) {
      scanf( "%d", k[i]+j );
      if( k[i][j] >= 1000 ) { // manje od 1000 slobodnih vremena, sve ih stavi rucno
        for( int l = k[i][j]; l <= T; l += k[i][j] )
          v[l].push_back( i*32 + j );
      } else { // period manji do 1000
        int x = k[i][j];
        for( int p = 0; p < cnt; ++p ) {
          int c = 0;
          while( x%prosti[p] == 0 ) x /= prosti[p], c++;
          while( c < MAXPOT ) postavi( faktor[p][c][i], j, 1 ), c++;
        }
      }
    }

  solveG();

  postavi( b[px], py, 1 );
  int ms1 = (1<<(n-1))-1, ms2 = (1<<(n-2))-1;

  for( int t = 0; t < T; ++t ) {
    // b - trenutna matrica
    // bb - sljedeca matrica
    // m - matrica blokiranosti sekunde t+1

    for( int i = 0; i < n; ++i ) {
      if( i-1 >= 0 ) bb[i-1] |= (b[i]>>2)|((b[i]&ms2)<<2);
      if( i-2 >= 0 ) bb[i-2] |= (b[i]>>1)|((b[i]&ms1)<<1);
      if( i+1 < n ) bb[i+1] |= (b[i]>>2)|((b[i]&ms2)<<2);
      if( i+2 < n ) bb[i+2] |= (b[i]>>1)|((b[i]&ms1)<<1);
    }

    for( int i = 0; i < n; ++i ) {
      b[i] = bb[i], bb[i] = 0;
      m[i] = (1<<n)-1;
    }

    int x = t+1, last = cnt-1;
    while( x > 1 && f[x] < 1000 ) {
      int now = ind[ f[x] ];

      if( last >= now+1 ) {
        for( int i = 0; i < n; ++i )
          m[i] &= fnula[now+1][last][i];
      }
      for( int i = 0; i < n; ++i )
        m[i] &= faktor[now][ fp[x] ][i];
      x /= ff[x], last = now-1;
    }
    if( last >= 0 )
      for( int i = 0; i < n; ++i )
        m[i] &= fnula[0][last][i];

    for( int i = 0; i < v[t+1].size(); ++i )
      postavi( m[ v[t+1][i]/32 ], v[t+1][i]%32, 1 );

    for( int i = 0; i < n; ++i )
      b[i] &= m[i];
  }

  int ans = 0;
  for( int i = 0; i < n; ++i )
    for( int j = 0; j < n; ++j )
      if( daj( b[i], j ) ) ans++;

  printf( "%d\n", ans );
  for( int i = 0; i < n; ++i )
    for( int j = 0; j < n; ++j )
      if( daj( b[i], j ) ) printf( "%d %d\n", i+1, j+1 );
  return 0;
}

COCI2011/2012 #1 skakac 状态压缩加速DP

上一篇:kali 操作磁盘和分区的命令


下一篇:安装docker+docker版NG+NG配置