思考メモ

ときたま、考えたことを出力する。

2024年1月の読書記録

普段は程々に小説をよんでいますが、生活の変化により、この月の読書量は増加しました。

『聖アウスラ修道院の惨劇』
『悪霊の館』
『幽霊屋敷』
『夜明け前の殺人』
『改訂・受験殺人事件』
メソポタミアの殺人』
『スイッチ 悪意の実験』
『推理対戦』
『刺青殺人事件』
邪馬台国の秘密』
邪馬台国はどこですか?
『新・世界の七不思議
『新・日本の七不思議』
『悲しみのイレーヌ』
ノワール・レヴナント』
『君の望む死に方』
『君に読ませたいミステリがあるんだ』
『最後のトリック』
『天久鷹の推理カルテ 吸血鬼の原罪』
『どんどん橋、落ちた』
『すみれ屋敷の罪人』
『生ける屍の死』

題名を並べると、内容がなんとなく思い出せます。思い出せなくなったら、再読すればいいのだ。

個人的に、歴史ミステリは新境地でした。 ついでに、邪馬台国の位置推定(比定)に興味を持ったので、そのうちまとめたい。

もとより、神社や寺、神話が好きなのです。『邪馬台国はどこですか?』に収められた聖徳太子厩戸皇子)の話も面白かった。

邪馬台国の秘密』では神功皇后の話が少し出てきます。神功皇后=卑弥呼説とかある(あった?)らしいですね。彼女についてWikiを読んでいたら、月岡芳年が描いた『大日本史略図会 第十五代神功皇后』が目に留まりました。かっこいいです。妊娠中になにやっとんねん、とは思いますが。

ja.ukiyo-e.org

というわけで『成吉思汗の秘密』も期待しています。ジンギスカンの話と思い手に取ったら、チンギス・ハンの話でした。期待していますよ、神津恭介。

Kotlinでクリエイティブコーディングする【備忘録】

ふと、Android で動作するデジタルアートを作りたいと思い立ち、衝動に身を任せて簡単なものを作った。そのとき学んだことを、忘れっぽい自分のためにメモしておく。

開発環境はAndroid Studio Electric Eel 2022.1.1

目次

準備
- アニメーション
- ブレンドモード
本編
- クリエイティブコーディング

アニメーション

一定時間ごとに、onDraw()を呼び出せば良い。invalidate()を使えば、onDraw()を強制的に呼び出すことができる。
Handlerを使う方法もあるが、今回はTimerAnimatorで実装してみる。

import ... //省略

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //タイトルバーを消す
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
        //レイアウトのセット
        val myView=MyView(this)
        setContentView(myView)
        //アニメーションの設定
        val anim = TimeAnimator()
        val fpm=(1000/60).toLong()
        anim.duration=fpm//単位はミリ秒
        anim.setTimeListener { _, _, _ ->
            myView.invalidate()//ここに一定時間ごとの処理を書く
        }
        anim.start()
    }
}

class MyView(context: Context): View(context){
    val paint: Paint = Paint()
    var radian=0.0

    override fun onDraw(canvas: Canvas){
        //値の更新
        radian+=PI/180

        val cx=(canvas.width/2).toFloat()
        val cy=(canvas.height/2).toFloat()
        val r=500F
        //キャンバスを塗りつぶす
        canvas.drawColor(Color.WHITE)
        //三角形
        paint.color = 0xFFFFC0CB.toInt()  //AARRGGBBで色を指定
        paint.style=Paint.Style.FILL_AND_STROKE
        val path = Path()  //多角形を描くにはPathを使う
        path.moveTo((r*cos(radian)+cx).toFloat(),
            (r*sin(radian)+cy).toFloat())
        path.lineTo((r*cos(radian+2*PI/3)+cx).toFloat(),
            (r*sin(radian+2*PI/3)+cy).toFloat())
        path.lineTo((r*cos(radian+4*PI/3)+cx).toFloat(),
            (r*sin(radian+4*PI/3)+cy).toFloat())
        path.close()  //FILLのときは省略可
        canvas.drawPath(path, paint)
        //矩形
        paint.color=Color.CYAN
        canvas.drawRect(cx-150F,cy-300F,cx,cy-150F,paint)
        paint.color=Color.YELLOW
        canvas.drawRect(cx-300F,cy-150F,cx-150F,cy,paint)
    }
}

実際はピンクの三角形が回転している。

見ていたアニメが分かる配色。これぞ、抽象芸術。多分、きっと、おそらく、ピカソも絶賛したに違いない。

TimerAnimatorの使い方は、見ての通り。durationTimeListenerを設定して、start()するだけ。
なお、上のsetTimeListenerの書き方は、

anim.setTimeListener(object:TimeAnimator.TimeListener{
    override fun onTimeUpdate(animation: TimeAnimator?, 
                              totalTime: Long, 
                              deltaTime: Long) {
         //なにか処理
     }
})

これをラムダ式で短くしたもの。

参考
- TimeAnimator  |  Android Developers

ブレンドモード

要するに、色を重ね合わせたときの効果。PorterDuffとも言う。
どのような種類の効果があるのかは、参考サイトに任せる。

override fun onDraw(canvas: Canvas){
        //省略
        //キャンバスのPorterDuffモードをクリアにする
        canvas.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR)
        //三角形
        //省略

        //ブレンドモードを設定
        paint.xfermode= PorterDuffXfermode(PorterDuff.Mode.DARKEN)
        paint.color=Color.CYAN
        canvas.drawRect(cx-150F,cy-300F,cx,cy-150F,paint)
        paint.color=Color.YELLOW
        canvas.drawRect(cx-300F,cy-150F,cx-150F,cy,paint)
        //設定を解除
        paint.xfermode=null
    }

これで、三角形と四角形の重なった部分にDARKENが適応される。

代わりに背景色が消えてしまったので、以下のように対処する。

//代わりのキャンバスを用意
val temp= createBitmap(width,height,Bitmap.Config.ARGB_8888)
val canvas2=Canvas()
canvas2.setBitmap(bmp)
canvas2.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR)

//canvasの代わりにcanvas2に描画
//省略

//canvas2をcanvasに貼る
canvas.drawColor(Color.WHITE)
canvas.drawBitmap(temp,0F,0F,null)

代わりのキャンバスを用意して、そこにブレンドモードで描画。その後に、本命のキャンバスに張り付けているだけ。

参考
- PorterDuffXfermode  |  Android Developers
- AndroidのCanvasを使いこなす! – PorterDuff – PSYENCE:MEDIA

クリエイティブコーディング

よさそうなサンプル探してきて改造してみる。
別の言語で書かれていても考え方は同じ。kotlinで書き直すだけなので、難しくない。

//MainActivity.kt
import ...

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //タイトルバーを消す
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)

        val myView=MyView(this)
        setContentView(myView)
        //アニメーションの設定
        val anim =TimeAnimator()
        val fpm=(1000/60).toLong()
        anim.duration=fpm
        anim.setTimeListener { _, _, _ ->
            myView.invalidate()
            myView.update()
        }
        anim.start()
    }

}
//MyView.kt
import ...

//描画本体
class MyView(context: Context): View(context){
    
    private val paint: Paint = Paint()
    
    private val starsNum=40
    private var stars= arrayListOf<Star>()
    //初期化
    init {
        for(i in 0 until starsNum){
            stars.add(Star(paint))
        }
        //
    }

    //描画関数
    @RequiresApi(Build.VERSION_CODES.Q)
    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas){
        val bmp= createBitmap(width,height,Bitmap.Config.ARGB_8888)
        val cbmp=Canvas()

        cbmp.setBitmap(bmp)
        cbmp.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR)

        draw2(cbmp)
        //背景塗りつぶし
        canvas.drawColor(Color.WHITE)
        canvas.drawBitmap(bmp,0F,0F,null)
    }

    //描画する
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun draw2(canvas: Canvas){
        paint.style=Paint.Style.FILL_AND_STROKE
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.LIGHTEN)

        for (i in 0 until starsNum)
            for (j in i + 1 until starsNum)
                for (k in j + 1 until starsNum) {
                    val s = stars[i]
                    val t = stars[j]
                    val u = stars[k]
                    if (s.kind == t.kind && t.kind == u.kind) {
                        if (s.distance(t) < 1000F && s.distance(u) < 1000F && t.distance(u) < 1000F) {
                            paint.color = s.col and 0x0FFFFFFF
                            val path = Path()
                            path.moveTo(s.x, s.y)
                            path.lineTo(t.x, t.y)
                            path.lineTo(u.x, u.y)
                            //path.close()//FILLのときは省略可
                            canvas.drawPath(path, paint)
                        }
                    }
                }

        paint.xfermode=null

        for(s in stars){
            s.drawCircle(canvas)
        }
    }

    //値更新
    fun update(){
        for (s in stars){
            s.update(this)
        }
    }

    class Star(val paint:Paint){

        var radius=Random.nextDouble(150.0,500.0)
        var cx=Random.nextDouble(-900.0,900.0)
        var cy=Random.nextDouble(-900.0,900.0)
        var direction= if(Random.nextBoolean())1.0 else -1.0
        var radian=Random.nextDouble(2*PI)

        var cr=150F
        var deg=Random.nextInt(360)
        var degd=if(Random.nextBoolean())1 else -1

        var x=0F
        var y=0F

        val kind= Random.nextInt(4)
        val cols=arrayOf(Color.MAGENTA,Color.YELLOW,Color.CYAN,Color.RED)
        var col=cols[kind]

        @RequiresApi(Build.VERSION_CODES.Q)
        fun drawCircle(canvas:Canvas){
            paint.color=col and 0x4FFFFFFF
            paint.style= Paint.Style.FILL
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.LIGHTEN)
            
            canvas.drawCircle(x,y,cr,paint)
        }

        fun update(layout:View){
            radian+=direction*2*PI*(1/radius)
            deg=if(deg+degd>0)(deg+degd)%360 else 360+degd
            x=(cos(radian)*radius+cx).toFloat()+layout.width/2
            y= (sin(radian)*radius+cy).toFloat()+layout.height/2
        }

        fun distance(s:Star): Float {
            val vx=x-s.x
            val vy=y-s.y
            return sqrt(vx*vx+vy*vy)
        }
    }

}

こんな感じの画面が作れた。

さらに、動画のフレーム切り出しと、GBの透過を行ったものがこちら。

この備忘録もそのうち書くが、gistのコードを貼っておく。
BtrArt1 · GitHub

感想

どちらかというと最近は、コーディングより、芸術的センスのほうが欲しいかも。

というわけで、これを買いました。
この記事には出てこないけどopenFrameworksいいよね。


クリエイティブコーディングは、コードさえ読めれば、どんな言語に対応できると思うので、気が向いたらこれで勉強します。

コラッツ数列の偶奇の順から、初期整数を求める

ある日の晩に考えた事の走り書き。

コラッツの問題

コラッツ予想、3n+1問題など、いくつか呼び名があるが、ここではコラッツの問題で通していく。

  • 任意の正の整数nに対し、nが偶数ならばnを2で割り、nが奇数ならば3をかけて1を足す操作を行う。

  • どのようなnに対しても、この操作を繰り返せば1に到達するか?

試しに、3でやってみる。

3->10->5->16->8->4->2->1

たしかに、1になった。

この操作によって得られた数列をコラッツ数列と呼ぶ。

本記事の内容

奇数nの3n+1は必ず偶数になる。これでは、任意の数列を考える上でめんどくさいので、以降は3n+1を$\frac{3n+1}{2}$に変更したコラッツショートカットを用いることにする。

コラッツショートカット、3の場合(かっこの中は、3n+1の値)

3->(10)5->(16)8->4->2->1

これを偶奇の順に直すと、奇奇偶偶偶偶となる。(1は除いた)

コラッツショートカットの操作は、整数nから数列を取り出す。

逆に、この偶奇の順から、nを導出する方法を考えてみたい。

問題文としてはこんな感じ。

任意の偶奇の順に対し、コラッツの問題が成り立つ初期整数nは求める方法は?

まあ、1から逆算すればすぐ求まるのだが、もう少し数式で考えてみる。

準備

<偶奇の順の表現>

偶数を0、奇数を1として、順を表現する。

奇奇偶偶偶偶->110000

そして、それを逆順にし、先頭に並ぶ0を消去する

110000->000011->11


(補足)
先頭に並ぶ0を消去したのは、(ある程度分かり切った)存在しない偶奇の順序を省く意図がある。
0を省略しなかった場合、例えば001(奇遇遇)の順序を持つnが存在しないことは、1から逆算すれば分かる。

n->(8)4->2->1

コラッツの問題のルールより、nは、3n+1=8となる奇数だが、そのようなnは存在しない。

また、01の並びを2進数として見ることで、一つの整数で表せる利点もある。

(その方がプログラムが書きやすいはず)


<$C_{ord}$関数>

$ f_1(n)=\frac{3n+1}{2}$、$f_0(n)=\frac{n}{2} $とし、
$f_1$と$f_0$をord順で合成したものを、$C_{ord}(n)$とする。

$C_{ord}(n)$は、ordの桁数をL、1の数をk、ある定数をAとして、次のように表せる。

$C_{ord}(n) = \frac{3^{k}n+A}{2^{L}}$

(数列の奇数の数だけ、3n+1が行われるので、分子、nの係数は3k
数列の長さだけ、n/2が行われるので、分母は2Lとなる)

例えば、ord=1010の場合、$C_{ord}(n)$は以下のようになる。

$C_{1010}(n)$
$=f_1(f_0(f_1(f_0(n))))$
$=f_1(f_0(f_1(n/2)))$
$=f_1(f_0(\frac{3(n/2)+1}{2}))$
$=f_1((\frac{3(n/2)+1}{2})/2)$
$=\frac{3((\frac{3(n/2)+1}{2})/2)+1}{2}$
$= \frac{3^{2}n+14}{2^{4}}$

式変形

偶奇の順ordから、nを求めるには、$C_{ord}(n)=2^{u}$をnについて解けばよい。

(コラッツ数列を求める操作において、2の累乗が出れば、あとは1になるまで偶数のみであるため)

以下、式変形

$\frac{3^{k}n+A}{2^{L}}=2^{u}$
$3^{k}n+A=2^{u-L}$

(1) $n=\frac{2^{u-L}-A}{3^{k}}$

これがnを求める式である

u-Lの値は、一つ変形前の式に対し、mod 3k をとることで、

(2) $2^{u-L}≡A \pmod{3^{k}}$

ordからA,k,が求められるので、この離散対数の式から、u-Lが求まる。

u-Lの値は一意ではないが、離散対数の周期性から、等差数列になる。

結論

偶奇の順ordから、整数nを求める手順は、

①ordから、$C_{ord}$関数のA,kを求める。

②$2^{u-L}≡A \pmod{3^{k}}$を用いて、u-Lを求める。

③$n=\frac{2^{u-L}-A}{3^{k}}$を用いて、nを求める。

u-Lが取り得る値は無限にあるので、nは無限に計算できる。

プログラム

pythonでコードを実装した。

from math import *

#離散対数
def DLP(q,p,m):      
    xs=[]
    for x in range(2*p):
        if(q**x%p==m%p):
            xs.append(x)
        if len(xs)==2:
            return xs[0],xs[1]-xs[0]

#Clz関数をordから求める
#return L,K,A
#Clz(N)=(3^K*N-A)/2^L
def ClzByOrd(ord):
    L,K,A=0,0,0
    while ord != 0:
        if ord%2==1:
            A=3*A+2**L
            K+=1
        L+=1
        ord=int(ord/2)
    return L,K,A

#OrdからNを求める
def NsByOrd(ord):
    L,K,A=ClzByOrd(ord)
    u_minus_L,T=DLP(2,3**K,A%3**K)
    Ns,ERs=[],[]
    #変数サイズが許す限り計算
    try:
        while True:
            N=int((2**u_minus_L-A)/3**K)
            #check
            if N>0:
                if ord==OrdByN(N):
                    Ns.append(N)
                else:
                    ERs.append(N)
            u_minus_L+=T
    except:
        return Ns,ERs

#Nからordを求める(チェック用)
def OrdByN(N):
    if N<=1:
        return None
    ord=0
    b=0
    while N !=1:
        if N%2==1:
            N=int((3*N+1)/2)
            ord+=2**b
        else:
            N=int(N/2)
        b+=1    
    return ord

if __name__=='__main__':
    #ordが0から1001まで,nを計算
    for ord in range(10):
        Ns,ERs=NsByOrd(ord)
        if len(Ns)==0:
            continue
        print(Ns[:10],bin(ord))

感想

計算するだけなら、深さ優先探索の方が速そう。(身も蓋もない)

離散対数が絡んでくるのは、個人的に楽しい発見だった。