JOI模擬予選に参加しました。
難易度はおおむね例年の予選と同じだが、問1が若干難しく、問3が簡単な印象。
備忘録ということで、コードを貼りつけておきます。
1 けんだま
問題
玉がA個、本体がB個ある。玉を1個増やすのにC[円]かかり、本体を1個増やすのにD[円]かかる。
予算X[円]で最大いくつのけん玉を作れるか。
解法
y個のけん玉を作るとする。
このときかかる金額は、C * max(0, y - A) + D * max(0, y - B)[円]である。
したがって、これがX以下となるようなyの最大値を出力すればよい。
コード
#include <iostream>
#include <algorithm>
using namespace std;
signed main()
{
int a, b, c, d, x;
int y;
cin >> a >> b >> c >> d >> x;
for( y = 0; y < 20000; y++ ){
int cost = c * max(0, y - a) + d * max(0, y - b);
if( cost > x )
break;
}
cout << y - 1 << endl;
return 0;
}
signedはintみたいなもの。
2 回文
問題
N文字の文字列S = s[1]s[2]…s[N]が与えられる。
3つの文字列
{s[i]s[i+1]…s[i+X-1]}
{s[i+X]s[i+X+1]…s[i+X+Y-1]}
{s[i+X+Y]s[i+X+Y+1]…s[i+2*X+Y]}
が全て回文となるようなiの個数を計算せよ。
解法
愚直にシミュレーションします。
文字列{s[l]s[l+1]…s[r-1]}が回文かどうかを判定する関数を作ると実装が楽でした。
コード
#include <iostream>
#include <string>
using namespace std;
int n, x, y;
string s;
bool ok( int l, int r ){
string t, u;
for( int i = l; i < r; i++ )
t += s[i];
for( int i = r - 1; i >= l; i-- )
u += s[i];
if( t == u )
return true;
return false;
}
signed main()
{
int i;
cin >> n >> x >> y;
cin >> s;
int ans = 0;
for( i = 0; i < s.length() - 2 * x - y + 1; i++ ){
if( ok(i, i + x) && ok(i + x, i + x + y) && ok(i + x + y, i + 2*x + y) )
ans++;
}
cout << ans << endl;
return 0;
}
3 渦巻きに歩く問題
問題
H行W列のグリッドがある。各マスには(1, 1), (H, W)などの座標が付けられている。
私は左上のマス(1, 1)から右渦巻きに移動していき、通ったマスに色を塗ることを繰り返す。
最初は青を塗り、次は赤、その次は青、というように青と赤を交互に塗っていく。
このとき、N個の座標(r1, c1), (r2, c2), …, (rN, cN)にあるマスはそれぞれ何色で塗られていたでしょうか。
青で塗られていたマスの数, 赤で塗られていたマスの数を空白区切りで出力せよ。
(制約)
1 <= H, W <= 109
1 <= N <= 1000
解法
ラフな考え
手で試すと、市松模様になることが分かる。
したがって、マスの座標を(a, b)とするとそのマスはa + bが偶数なら青、a + bが奇数なら赤で塗られている。
厳密な考え
移動先のマスの座標を(a, b)すると、移動するごとにa + bの偶奇が入れ替わる。
塗る色も入れ替わる。最初はa + bは偶数で塗る色は青である。
したがって、マスの座標を(a, b)とするとそのマスはa + bが偶数なら青、a + bが奇数なら赤で塗られている。
というわけで、(ri + ci) % 2 == 0となるiの個数、(ri + ci) % 2 == 1となるiの個数を計算すればよい。
コード
#include <iostream>
using namespace std;
int h, w, n;
int r, c;
signed main()
{
int i;
cin >> h >> w >> n;
int bcnt = 0;
int rcnt = 0;
for( i = 0; i < n; i++ ){
cin >> r >> c;
if( (r + c) % 2 == 0 )
bcnt++;
else
rcnt++;
}
cout << bcnt << " " << rcnt << endl;
return 0;
}
4 鉄道に乗る問題
問題
駅1, 2, 3, …, Nがこの順で並んでいる。
私は時刻0に駅Sにいる。いくら時間がかかってもよいので駅Tにつきたい。そのような電車の乗り方が何通りあるかを計算せよ。
ただし、乗り換え時間は無視できるものとする。電車の情報は以下の通り。
電車はM本ある。
i本目の電車は時刻A[i]に駅B[i]を発車し、駅B[i] + C[i], B[i] + 2C[i], …の順で止まる。なお、到着時刻(=発車時刻)はそれぞれA[i] + C[i], A[i] + 2C[i], …となる。
(制約)
1 <= N <= 100
1 <= M <= 100
1 <= A[i] <= 100
1 <= B[i] <= N
1 <= B[i] + C[i] <= N
(C[i]が負になる場合もある)
解法
次に乗れる電車は、現時点での(時刻, 駅, さきほど降りた電車)から分かる。
また、この3つを持っておけば、電車Xに乗って駅Yで降りた直後の(時刻, 駅, さきほど降りた電車)も分かる。
今いる駅 = Tになれば駅に行けたことが分かる。
したがって、(時刻, 駅, さきほど降りた電車の番号)を引数にした再帰関数を組むことができる。
ただし、再帰関数を愚直に組むと時間超過するので、一工夫する。
同じ(時刻, 駅, さきほど降りた電車の番号)場合の再計算が無駄なので、最初に計算した結果を配列にメモしておき、
再び同じ(時刻, 駅, さきほど降りた電車の番号)に遷移したときは、その結果を返すようにする。
つまり、(時刻, 駅, さきほど降りた電車の番号)を状態としてDPする。
遷移(X, Yを決めるところ)は複雑なので注意。
コード
#include <iostream>
#include <cmath>
using namespace std;
int n, m, s, t;
int a[100], b[100], c[100];
int dep; //デバッグ用(探索の深さ)
int dp[201][101][100]; //dfs(jikoku, eki, ressya)の返した値
//「時刻jikokuに駅ekiにいて列車ressyaから降りた」状態から「駅tへ行く方法の数」を返す
int dfs( int jikoku, int eki, int ressya ){
int i, j;
int ret = 0;
/*for( i = 0; i < dep; i++ ) cout << "・";
cout << " 時刻" << jikoku << " 駅" << eki;
cout << endl;*/
if( ressya >= 0 && dp[jikoku][eki][ressya] != -1 )
return dp[jikoku][eki][ressya];
if( eki == t )
ret++; //駅tについたから1通り加算する
for( i = 0; i < m; i++ ){
//降りた直後に同じ電車には乗れない
if( i == ressya )
continue;
//駅ekiに停車しない電車には乗れない
if( (eki - b[i]) / c[i] < 0 || abs(eki - b[i]) % abs(c[i]) != 0 )
continue;
//駅ekiを通過してしまった電車には乗れない
if( jikoku > a[i] + abs(b[i] - eki) )
continue;
//電車iに乗って駅jで降りる
for( j = eki + c[i]; 1 <= j && j <= n; j += c[i] ){
dep++;
ret += dfs(a[i] + abs(b[i] - j), j, i);//次の駅からの行き方を足す
ret %= 100000; //逐次余りを取る
dep--;
}
}
dp[jikoku][eki][ressya] = ret;
return ret;
}
signed main()
{
int i;
cin >> n >> m >> s >> t;
for( i = 0; i < m; i++ ){
cin >> a[i] >> b[i] >> c[i];
}
for( i = 0; i < 201; i++ ){
for( int j = 0; j < n + 1; j++ ){
for( int k = 0; k < m; k++ ){
dp[i][j][k] = -1;
}
}
}
cout << dfs(0, s, -1) << endl;
return 0;
}
もっときれいなコードを書きたかった。
想定解とは少し違うらしい。
5 楽しい楽しい花の水やり
問題
花1, 2, …, Nがある。0日目の花iのみずみずしさはA[i]である。花のみずみずしさは日付が変わる瞬間にB[i]減少する。
みずみずしさが0以下になった花は枯れてしまう。
私は好きな時刻に何度でも「みずやり」をすることができる。「みずやり」は次の2ステップで行われる。
1.整数iを決める
2.花i - K, i - K + 1, …, i + KのみずみずしさをC増やす。
T日目にどの花も枯れていないようにしたい。しかし「みずやり」も重労働なので「みずやり」の回数もなるべく少なくしたい。
「みずやり」の回数の最小値を計算せよ。
(制約)
1 <= N <= 5 * 10 ^ 6
1 <= T <= 10 ^ 7
1 <= K <= N / 2
1 <= A[i], B[i] <= 10 ^ 7
1 <= C <= 10 ^ 7
なお、a ^ bはaをb回かけたものである。
(補足)
A[i], B[i]は乱数で生成する。生成の仕方はコードを見て察してください。
解法
まず、全ての入力はlong long型変数で管理したほうが良い。
考察1
水やりを全く行わなかったら、花iのみずみずしさは、A[i] - B[i] * Tとなる。
花iにX[i]水がかかったら、花iのみずみずしさは、水やりをしたタイミングによらず
A[i] - B[i] * T + C * X[i]
となる。
少なくともこれが0を超えている必要がある。逆に、これが0になるような最小のX[i]をMx[i]としたとき、
花iにはMx[i]回以上水がかかれば良い。
水やりは0日目にまとめて行おう。なお、水やりの中心位置は
花1 + K, 2 + K, …のようにずらしていくのが最善である。
具体的には、
花1~1 + 2Kに「みずやり」をする。
花1にMx[1]回以上水をやったら(花1が枯れなくなったら)花2~2 + 2Kに「みずやり」をする。
花2にMx[2]回以上水をやったら(花2が枯れなくなったら)花3~3 + 2Kに「みずやり」をする。
~
を繰り返せばよい。
これを愚直にやるとO(NK)で答えを計算できる。
考察2
上記の「みずやり」シミュレーションを高速化したい。具体的には
・X[i]の値を求める
・X[i+1]~X[i+2K]の値をX[i]減らす
というクエリを高速に処理したい。
これは区間加算1点クエリのセグメントツリーでできる。
もしくは累積和を行えばよい。私はセグメントツリーで解いたが、累積和(いもす法)を使えば
実行が速く実装も楽である。
コード
#include <iostream>
#include <algorithm>
#define int long long //intがlonglongに置換される
using namespace std;
int N, K, C, T;
int Sa, Xa, Ya, Za;
int Sb, Xb, Yb, Zb;
void input(){
cin >> N >> K >> C >> T;
cin >> Sa >> Xa >> Ya >> Za;
cin >> Sb >> Xb >> Yb >> Zb;
}
void generate(int A[], int B[]){
for( int i = 1; i <= N; i++ ){
A[i] = Sa + 1;
B[i] = Sb + 1;
Sa = (Xa * Sa + Ya) % Za;
Sb = (Xb * Sb + Yb) % Zb;
}
}
int A[5000001], B[5000001];
int X[5000001]; //X[i] = 花iに水をかける回数の最小値
const int depth = 23;
class Segtree{
public:
int data[20000000];
void init(){
for( int i = 0; i < 20000000; i++ ) data[i] = 0;
}
void add( int l, int r, int v, int a = 0, int b = (1 << depth) - 1, int ind = 0, int dep = 0 ){
if( dep > depth ) return;
if( b < l || a > r ) return;
if( l <= a && b <= r ){
data[ind] += v;
}
else{
add( l, r, v, a, a + (b - a + 1) / 2 - 1, ind * 2 + 1, dep + 1 );
add( l, r, v, a + (b - a + 1) / 2, b, ind * 2 + 2, dep + 1 );
}
}
int getNum( int x ){
int ret = 0;
x += (1 << depth) - 1;
ret += data[x];
while( x > 0 ){
x = (x - 1) / 2;
ret += data[x];
}
return ret;
}
}seg;
void generateX(){
for( int i = 1; i <= N; i++ ){
//A[i] - B[i] * T + C * X[i] > 0となるX[i]の最小値を求める
X[i] = max((int)0, (B[i] * T - A[i]) / C + 1);
}
}
void segInit(){
seg.init();
for( int i = 1; i <= N; i++ ){
seg.add(i, i, X[i]);
}
}
//花1 + Kを中心に水をかける→花2 + Kを中心に水をかける→…
//水やり回数を返す
int solve(){
int center;
int ans = 0;
for( center = 1 + K; center <= N + K; center++ ){
int left = center - K;
int right = min(N, center + K);
//max(0, X[left])回水やりをする
//①max(0, X[left])を求める
int kaisu = max((int)0, seg.getNum(left));
//②答えを更新
ans += kaisu;
//③X[left]~X[right]に-max(0, X[left])足す
seg.add(left, right, -max((int)0, kaisu));
}
return ans;
}
signed main()
{
input();
generate(A, B);
generateX();
segInit();
cout << solve() << endl;
return 0;
}
高級な道具を使ってしまった。
6 完全に忘れました
ごめんなさい
感想
JOI予選模擬にもかかわらずガチ参加できるほど良問ぞろいでした。
部外者が勝手に参加してすみませんでした。
運営陣は素晴らしい。はっきりわかんだね。