2011年5月31日火曜日

個数制限なしナップサック問題 in Elisp

ナップサック問題の基本形には「n種類の品物が1個ずつ」という条件が付いていた。よって、この条件を無くした問題を考えよう、という話が自然に出てくる。

条件の有無を区別するため、前者を「0-1ナップサック問題」、後者を「個数制限なしナップサック問題」とか "UKP: unbounded knapsack problem" と呼ぶのが一般的らしい。

何も考えないで入力を与えると両者の違いが現れないこともあるが、たとえば (重さ, 価値) のリストが((2 2) (3 4) (5 7))、重さの制限W=9 と与えると次のように出力に違いが出てくる。

0-1ナップサック問題の場合
答え 11 … (3 4) と (5 7) の要素を選択するので 4 + 7 で 11
個数制限なしナップサック問題の場合
答え 12 … (3 4) の要素を3個選択するので 4 × 3 で 12

以下、個数制限なしのほうを解くプログラムを考える。と言っても 0-1ナップサック問題を再帰的に解いている場合は少し変更するだけで個数制限なしのほうも解ける。

;; 0-1ナップサック問題
(defun knapsack (lst w)
  (cond 
   ((null lst) 0)
   (t (if (> (caar lst) w) ;先頭の品物が入らない場合
          (knapsack (cdr lst) w)
        (max  ;入れる場合と入れない場合を両方とも試し、良い結果のほうを選ぶ
         (+ (cadar lst) (knapsack (cdr lst) (- w (caar lst))))
         (knapsack (cdr lst) w))))))

上のプログラムの後半部分((max)の中)で、先頭の品物を何度も入れるようにする。具体的には、

        (max  ;入れる場合と入れない場合を両方とも試し、良い結果のほうを選ぶ
         (+ (cadar lst) (knapsack (cdr lst) (- w (caar lst))))
         (knapsack (cdr lst) w))))))

を次のように変える。

        (max  ;先頭の品物を繰り返し入れる場合と、入れない場合を試して良いほうを選ぶ
         (+ (cadar lst) (knapsack lst (- w (caar lst))))
         (knapsack (cdr lst) w))))))

要するに (cdr lst)lstに変え、コメントを編集しただけ。

最後に確認。

(defun knapsack (lst w)
  (cond 
   ((null lst) 0)
   (t (if (> (caar lst) w) ;先頭の品物が入らない場合
          (knapsack (cdr lst) w)
        (max  ;先頭の品物を繰り返し入れる場合と、入れない場合を試して良いほうを選ぶ
         (+ (cadar lst) (knapsack lst (- w (caar lst))))
         (knapsack (cdr lst) w))))))
=> knapsack

(knapsack '((2 2) (3 4) (5 7)) 9)
=> 12

この関数の引数を眺めてみると、最大で n × W 種類しかないことが自然に分かる。つまり個数制限無しの場合も、0-1の場合と同じく n × W のメモ化テーブルに結果を保持することで計算量を減らせる。

2011年5月26日木曜日

メモ化再帰: symbol-function を使う方法

Elispで再帰的関数をメモ化する方法の続き。

Y Combinator による方法のほかにも、symbol-function を使って関数定義を書き換える方法がある。

symbol-functionを使ったメモ化再帰の例(Elisp)

(defun memo (fn name key test)
  "Return a memo-function of fn."
  (lexical-let ((_fn fn) (_key key) (table (make-hash-table :test test)))
    #'(lambda (&rest args)
        (let ((k (funcall _key args)))
          (if (gethash k table)
              (gethash k table)
            (setf (gethash k table) (apply _fn args)))))))
=> memo

(defun* memoize (fn-name &key (key #'first) (test #'eql))
  "Replace fn-name's global definition with a memoized version."
  (setf (symbol-function fn-name)
        (memo (symbol-function fn-name) fn-name key test)))
=> memoize

;;; フィボナッチ数列で試す
(defun fib (n)
  (if (<= n 1) 1
    (+ (fib (- n 1)) (fib (- n 2)))))
=> fib

;;; 実行時間をはかる関数
(defun profile (func &rest args)
  (let ((start (current-time)))
    (apply func args)
    (time-to-seconds (time-subtract (current-time) start))))
-> profile

;;; メモ化なしの場合の実行時間
(profile 'fib 25)
=> 0.124392

;;; fibをメモ化する
(memoize 'fib)
=> (lambda (&rest --cl-rest--) (apply (lambda (G90287 G90288 G90289 &rest args) (let ... ...)) (quote --table--) (quote --_key--) (quote --_fn--) --cl-rest--))

;;; メモ化した場合の実行時間
(profile 'fib 25)
=> 0.000166

ちなみにこの方法は、"Paradigms of Artificial Intelligence Programming" というCommon Lispの本に載っていた(9.1 Caching Results of Previous Computations: Memoization)。そのプログラムを、 defun*, lexical-let等を利用してEmacs Lisp 用に調整しただけ。

2011年5月25日水曜日

Longest Common Subsequence の長さ in Elisp

Common Subsequence 解説 や、 最長共通部分列問題 (Longest Common Subsequence) - naoyaのはてなダイアリー で紹介されている問題。動的計画法やメモ化で計算量が減らせる例として知られている、とのこと。

(defun lcs-length (l1 l2)
  (cond
   ((or (null l1) (null l2)) 0)
   ((eq (car l1) (car l2))
    (+ (lcs-length (cdr l1) (cdr l2)) 1))
   (t
    (max
     (lcs-length l1 (cdr l2))
     (lcs-length (cdr l1) l2)))))
=> lcs-length

(lcs-length '(a b c b d a b) '(b d c a b a))
=> 4

2011年5月24日火曜日

ナップサック問題とメモ化 in Elisp

有名なナップサック問題を Emacs Lisp で勉強である。

ナップサック問題とは「重さと価値がそれぞれ wi, vi, である品物から、重さの総和が W を超えないように選んだときの、価値の総和の最大値を求める」問題である。

素朴なバージョン

価値の最大値を求める関数を作る。入力は ((w1, v1) (w2, v2) ... (wn, vn)) なる形のリストと、総和 W の2つ。

(defun knapsack (lst w)
  (cond 
   ((null lst) 0)
   (t (if (> (caar lst) w) ;先頭の品物が入らない場合
          (knapsack (cdr lst) w)
        (max  ;入れる場合と入れない場合を両方とも試し、良い結果のほうを選ぶ
         (+ (cadar lst) (knapsack (cdr lst) (- w (caar lst))))
         (knapsack (cdr lst) w))))))
;; 確認
(knapsack '((10 7) (4 2) (5 9) (1 4) (7 9) (3 7) (6 4) (3 5)) 20)
=> 34

n個の品物それぞれについて、入れる/入れないの二通りを計算する考え方であり、最大計算量は O(2n) となる。

※確認には ナップサック問題とは? に載っている例を使っている。

※Emacs のバージョンは GNU Emacs 22.3.1

メモ化(memoization)を行うバージョン

上の関数 knapsack が再帰的に実行されるときの引数を分析すると、第一引数は長さnのリストから先頭の要素が一つずつ取り去られていくので合計 n種類になる。また、第二引数は初期値 W から何らかの整数を引いた値であるから、最大で W - 1 から 0 までの整数の数、つまりW種類になる。

第一引数が n 通りで第二引数が W 通りなので、引数の場合の数は結局のところ n * W 通り。メモ化を利用して n * W程度の領域に結果を保存すれば、最大計算量を 2n から n * W のオーダーに減らすことができるわけである。

次に、具体的にどうやってメモ化を行うかを考える。素朴な方法は、グローバル変数としてハッシュテーブルを定義しておき、knapsackの中でそれを参照するというもの。しかし、この方法は関数をグローバル変数に依存した形に書き換えなければならず、再利用やコピペがしにくくなる。

そこで、もう少し独立性を保てるよう、Yコンビネータを利用した方法を使う。この方法については、 さあ、Yコンビネータ(不動点演算子)を使おう! - よくわかりません を参考に、YコンビネータのコードはY combinator - Rosetta Code の中のCommon Lispバージョンを参考にするとよい(Elispでも動くように let ではなく lexical-let を使った形に書き換えるなどする)。

;; メモ化機能を組み込んだ Y です
(defun Y (f)
  (lexical-let ((_f f) (cache (make-hash-table :test #'equal)))
    ((lambda (x) (funcall x x))
     (lambda (y)
       (lexical-let ((_y y))
         (funcall _f (lambda (&rest args)
                       (if (gethash args cache)
                           (gethash args cache)
                         (setf (gethash args cache)
                               (apply (funcall _y _y) args))))))))))

;; Yコンビネータ向けに、ラムダ式を返す形
(defun knapsack-memo (f)
  (lexical-let ((_f f))
    (lambda (lst w)
      (cond 
       ((null lst) 0)
       (t (if (> (caar lst) w)
              (funcall _f (cdr lst) w)
            (max
             (+ (cadar lst) (funcall _f (cdr lst) (- w (caar lst))))
             (funcall _f (cdr lst) w))))))))

;; 確認
(funcall
 (Y 'knapsack-memo)
 '((10 7) (4 2) (5 9) (1 4) (7 9) (3 7) (6 4) (3 5)) 20)
=> 34

以下は、計算時間を比較した例。

;; 時間をざっと計る関数
(defun profile (func &rest args)
  (let ((start (current-time)))
    (apply func args)
    (time-to-seconds (time-subtract (current-time) start))))
=> profile

;; 重さと価値のリスト
(setq *w-v-list* '((10 7) (4 2) (5 9) (1 4) (7 9) (3 7) (6 4) (3 5) (3 5) (5 3) (1 2) (3 4) (1 5) (2 2) (4 5) (2 2) (4 5) (3 9) (1 5)))
=> ((10 7) (4 2) (5 9) (1 4) (7 9) (3 7) (6 4) (3 5) (3 5) (5 3) (1 2) (3 4) ...)

;; 素朴なバージョンは約 0.43 秒
(profile 'knapsack *w-v-list* 30)
=> 0.438918

;; メモ化バージョンは約 0.06 秒
(profile (Y 'knapsack-memo)  *w-v-list* 30)
=> 0.063991

n をもっと増やすと劇的に差が出るはず。ちなみに手軽に時間を計る関数は実行時間の測定 どう書く?org から流用したもの。

2011年5月22日日曜日

ハフマン符号 in Elisp

ハフマン符号化法のプログラムは「計算機プログラムの構造と解釈」(SICP)を参考にするとよい。完全な形では載っていないので、演習問題をいくつか解く必要はあるが。

概要

任意の文字列をハフマン符号化法によって符号化したり復号化したりしたい。これに必要になる関数は、大雑把に挙げると次の4つ。

  1. ハフマン木を作る関数(文字と頻度の組の集合を入力 ⇒ ハフマン木を出力)
  2. 符号化する関数(符号化したい文字列とハフマン木を入力 ⇒ 符号を出力)
  3. 復号化する関数(符号とハフマン木を入力 ⇒ 元の文字列を出力)
  4. 文字列内の文字の出現頻度を求める関数

以下、それぞれのコードをEmacs lispで書いていく。

ハフマン木を作る関数

データ構造はSICPに書かれているとおりで、木と葉の二種類を組み合わせてハフマン木を構成する。

木のほうは、

  • 左のノード
  • 右のノード
  • 文字の集合
  • 頻度の合計値

…の合計4要素。葉のほうは、

  • 葉であることを表すシンボル leaf
  • 文字
  • 文字の頻度

…の合計3要素から成るリスト。

木は関数 make-code-tree で、葉は関数 make-leaf で生成する。そのほかに、木/葉に含まれる文字の集合を集める関数 symbols や頻度の合計を求める関数 weight 等を書いたのが以下のコード。

(defun make-leaf (symbol weight)
  (list 'leaf symbol weight))

(defun leafp (object)
  (eq (car object) 'leaf))

(defun symbol-leaf (x)
  (cadr x))

(defun weight-leaf (x)
  (caddr x))

(defun symbols (tree)
  (if (leafp tree)
      (list (symbol-leaf tree))
    (caddr tree)))

(defun weight (tree)
  (if (leafp tree)
      (weight-leaf tree)
    (cadddr tree)))

(defun make-code-tree (left right)
  (list left
        right
        (append (symbols left) (symbols right))
        (+ (weight left) (weight right))))
;; ちょっと確認
(make-code-tree (make-leaf 'b 1) (make-leaf 'c 2))
=> ((leaf b 1) (leaf c 2) (b c) 3)

続いて、 ((c 3) (b 2) (a 1)) のような形で文字と出現頻度の組が与えられると、それをハフマン木に変換する関数 generate-huffman-tree を書く。ここで、make-leaf-set((c 3) (b 2) (a 1)) の各要素を葉に変換しつつ adjoin-set を利用して頻度の昇順に並べかえ、successive-merge は昇順のリストから先頭の葉/木を2つ取り出して、つまり貪欲法的に選択して、木を構成していく関数。

(defun generate-huffman-tree (pairs)
  (successive-merge (make-leaf-set pairs)))

(defun adjoin-set (x set)
  (cond ((null set) (list x))
        ((< (weight x) (weight (car set))) (cons x set))
        (t (cons (car set)
                 (adjoin-set x (cdr set))))))
;; ちょっと確認
(adjoin-set (make-leaf 'a 4)
            (list (make-code-tree (make-leaf 'b 2) (make-leaf 'c 1))))
=> (((leaf b 2) (leaf c 1) (b c) 3)
    (leaf a 4))

(defun make-leaf-set (pairs)
  (if (null pairs)
      '()
    (let ((pair (car pairs)))
      (adjoin-set (make-leaf (car pair)    ; symbol
                             (cadr pair))  ; frequency
                  (make-leaf-set (cdr pairs))))))
;; ちょっと確認
(make-leaf-set '((c 3) (b 2) (a 1)))
=> ((leaf a 1)
    (leaf b 2)
    (leaf c 3))

(defun successive-merge (set)
  (cond ((null set) '())
        ((= (length set) 1) (car set))
        (t (successive-merge
            (adjoin-set (make-code-tree (car set) (cadr set))
                        (cddr set))))))

;; ハフマン木を生成して確認
(generate-huffman-tree
  '((e 5) (d 4) (c 3) (b 2) (a 1)))
=> (((leaf c 3)
     ((leaf a 1)
      (leaf b 2)
      (a b)
      3)
     (c a b)
     6)
    ((leaf d 4)
     (leaf e 5)
     (d e)
     9)
    (c a b d e)
    15)
;; 木を図にしてみると、問題なさそう
     15
   6    9
  3 3  4 5
   1 2

符号化する関数(符号化したい文字列とハフマン木を入力 ⇒ 符号を出力)

encode という関数を作る。符号化したい文字列は、(A D A B B C A) のようなリストの形で与え、一文字を符号化する関数 encode-symbol を補助関数として使用する。そうすると、コードは次のようになる。

(defun encode (message tree)
  (if (null message)
      '()
    (append (encode-symbol (car message) tree)
            (encode (cdr message) tree))))

(defun encode-symbol (symbol tree)
  (if (member symbol (symbols tree))
      (cond ((leafp tree) nil)
            ((member symbol (symbols (left-branch tree)))
             (cons 0 (encode-symbol symbol (left-branch tree))))
            (t (cons 1 (encode-symbol symbol (right-branch tree)))))
    (error "the symbol %S doesn't exist in the tree -- ENCODE-SYMBOL" symbol)))

;; ハフマン木を仮に定義して確認
(setq *my-test-tree*
      (make-code-tree (make-leaf 'A 4)
                      (make-code-tree
                       (make-leaf 'B 2)
                       (make-code-tree (make-leaf 'D 1)
                                       (make-leaf 'C 1) ))))
=> ((leaf A 4) ((leaf B 2) ((leaf D 1) (leaf C 1) (D C) 2) (B D C) 4) (A B D C) 8)

(encode-symbol 'A *my-test-tree*)
=> (0)

(encode-symbol 'D *my-test-tree*)
=> (1 1 0)

(encode-symbol 'E *my-test-tree*)
=> error

(encode '(A D A B C) *my-test-tree*)
=> (0 1 1 0 0 1 0 1 1 1)

復号化する関数(符号とハフマン木を入力 ⇒ 元の文字列を出力)

decode 関数を作る。符号は、(0 1 1 0 0 1 0 1 1 1) のようなゼロと1のリストで、出力も(A D A B C)のようなリスト。メインの decode は、flet で定義した補助関数 decode-1 を呼び出す形。ビットを先頭から取りだして木を探索し、葉に到達したらその葉が持つ文字を自然なる再帰にcons する。そのほかに、left-branch, right-branch, choose-branch も定義。

(defun decode (bits tree)
  (flet ((decode-1
          (bits current-branch)
          (if (null bits)
              '()
            (let ((next-branch (choose-branch
                                (car bits)
                                current-branch)))
              (if (leafp next-branch)
                  (cons (symbol-leaf next-branch)
                        (decode-1 (cdr bits) tree))
                (decode-1 (cdr bits) next-branch))))))
    (decode-1 bits tree)))

(defun left-branch (tree) (car tree))

(defun right-branch (tree) (cadr tree))

(defun choose-branch (bit branch)
  (cond ((= bit 0) (left-branch branch))
        ((= bit 1) (right-branch branch))
        (t (error "bad bit %S -- CHOOSE-BRANCH" bit))))

;; 既出の例で確認
(decode '(0 1 1 0 0 1 0 1 1 1) *my-test-tree*)
=> (A D A B C)

文字列内の文字の出現頻度を求める関数

最後に、文字列を調査して文字と頻度のリストを生成する関数count-occurences を作る。これの出力を上述のgenerate-huffman-tree に渡すことになる。

概要としては、loopマクロで文字と頻度から成る連想リスト(association list)を作っておき、mapcarによってドット対をリストに変換する、という流れになっている。

(defun count-occurences (str)
  (let ((n (length str)) tbl)
    (loop for i from 0 to (1- n) do
          (if (assoc (elt str i) tbl)
              (incf (cdr (assoc (elt str i) tbl)))
            (setf tbl (cons (cons (elt str i) 1) tbl))))
    (mapcar (lambda (x)
              (list (car x) (cdr x))) tbl)))

;; 確認(文字コード 97 が a に対応)
(count-occurences "aaa bb c")
=> ((99 1) (98 2) (32 2) (97 3))

統合

最終確認。

;; 文字列
(setq *my-message* "BACADAEAFABBAAAGAH")
=> "BACADAEAFABBAAAGAH"

;; 文字列からHuffman木を作る
(setq *my-h-tree* (generate-huffman-tree
    (count-occurences *my-message*)))
=> ((leaf 65 9) (((... ... ... 2) (... ... ... 2) (67 68 69 70) 4) ((... ... ... 2) (leaf 66 3) (71 72 66) 5) (67 68 69 70 71 72 66) 9) (65 67 68 69 70 71 72 66) 18)

;; 符号化(string-to-list で文字列を文字のリストに変換している)
(setq *my-encoded-message*
      (encode (string-to-list *my-message*) *my-h-tree*))
=> (1 1 1 0 1 0 0 0 0 1 0 0 ...)

;; ビット長さ
(length *my-encoded-message*)
=> 42

;; 復号化(string をapply して文字のリストを文字列に変換)
(apply 'string (decode *my-encoded-message* *my-h-tree*))
=> "BACADAEAFABBAAAGAH"
  

以上、貪欲法の例としてのハフマン符号化。とても素朴な実装と思われる(性能等を考えると改善の余地がたくさんある)。

2011年5月17日火曜日

柵の修理問題 Fence Repair Problem

3253 -- Fence Repairという問題をやってみるテスト。

問題
農家のJohnが農場の柵を修繕しようと計画したところ、L1, L2, ..., LNの長さをもつN枚の板が必要だと分かった。
彼は、L1からLNまでの総和に等しい長さの板を一枚買い、それをノコギリで切り分けることにした。
しかし、ノコギリを持っていないことに気づいたので、知りあいの農家Donにノコギリを貸してくれるよう打診した。
資本家でもあるDonはノコギリを貸さず、自分が板を切る作業をN-1回行う代わりに各回ごとに料金を請求したいと提案した。各回の料金とは、そのときに切断する板の長さと同じ。すなわち、長さ21の板を板に切り分ける場合は21となる。
この料金の考え方に従うと、板を切る順番によって最終的な請求額が変化する。Johnが支払うべき最小の金額を求めよ。

解くための考え方

例として長さ 21 の板から長さ 8, 5, 8 の3つの板に分ける場合を考えると、次の二通りの分け方がある。

  1. まず 13 と 8 に分け、次に長さ 13 の板を 8 と 5 に分ける。
  2. まず 16 と 5 に分け、次に長さ 16 の板を 8 と 8 に分ける。

これを図示したのが次の図。1番目の料金が 21 + 13 = 34 となり、2番目は 21 + 16 = 37 となるので、1番目のほうが安く上がる。

大雑把な分析だが、図はデータ構造で言うところの二分木の形になり、木の下のほうに短い板があったほうが全体のコストが小さくなる。したがって、問題を解くためには、N枚の板の中からできるだけ短い板を集めながらボトムアップに木を構築していけばよい。その様子を示したのが下図。

短い板を優先して集めるので、これも「貪欲法」のアルゴリズムと言える。また、木の構成方法が「ハフマン符号化法」で使う「ハフマン木」と同じになっている。ただし、この問題ではコストの合計値が分かればよいので、木を完全に構成する必要はない。

コードと実行例(Emacs Lisp で)

入力として L1, L2, ..., LN をリストで渡すと、結果(最小の料金)を返すような関数にした。

(defun fence-repair (lst)
  "メインの関数です"
  (cond
   ((null lst) 0)
   ((= (length lst) 2) (apply '+ lst))
   (t
    (multiple-value-bind (pos-min pos-next-min)
        (position-smallest-two lst)
      (let ((sum (+ (nth pos-min lst) (nth pos-next-min lst))))
        (+ sum
           (fence-repair
            (remove-nth
             pos-min
             (replace lst (list sum) :start1 pos-next-min) ))))))))

(defun remove-nth (n list)
  "リストからn番目の要素を消す"
  (if (zerop n)
      (cdr list)
    (cons (car list) (remove-nth (1- n) (cdr list)))))

(defun position-smallest-two (list)
  "最小値をもつ要素と、2番目に小さい値をもつ要素の位置を返す"
  (let* ((min 0) (next-min 1) (n (length list)) (i 1))
    (while (< i n)
      (cond
       ((< (nth i list) (nth min list))
        (setf next-min min) (setf min i) )
       ((< (nth i list) (nth next-min list))
        (setf next-min i) ))
      (incf i) )
    (list min next-min) ))

以下は実行例で、それぞれ (8, 5, 8)(1 1 2 2 3 3) の場合。

(fence-repair '(8 5 8))
=> 34

(fence-repair '(1 1 2 2 3 3))
=> 30
;;おそらく以下のような二分木になるはず (+ 12 7 5 4 2) で 30
       12
    7      5
   4  3   2  3
 2   2
1  1

今度はハフマン符号化のプログラムをELispで作ってみたい。SICPにヒントが載ってるし。

2011年5月11日水曜日

「新米管理者のためのサーバが重くて困ったとき読む本」との別れあるいはホマゲ(homage, オマージュ)

本棚を整理していると出てきた、なつかしい本。

ざっと目を通してみたが、難解だと思う部分は無かった。

今後読み返すことはないだろう。便利なコマンドだけここに記録して、心おきなく捨てたいと思う。

便利コマンドその1: ディスクの応答速度を調べる - iostatコマンド

以下は10秒間隔で表示する例。

$ iostat -C -d 10
          disk0       cpu
    KB/t tps  MB/s  us sy id
   48.14   3  0.15  10  6 83
   15.93   3  0.05  14  7 79
  

ちなみにオプション無しで実行すると disk, cpu のほかにロードアベレージも表示される。オプション無しで実行するほうが普通だと思われるが、この本にはなぜか-C -dを指定して表示を抑制した例が掲載されている。

出力の意味
ディスクCPU
KB/ttpsMB/sussyid
転送命令あたり転送量(KB) 1秒あたり転送命令数 1秒たりの転送量(MB) ユーザープロセスによる使用率 システムプロセスによる使用率 アイドル率

便利コマンドその2: ディスクのエラー検出 - dmesgコマンド

通常、ディスクの不具合は、カーネルのエラーとして検出されることが多くあります。つまり、dmesgコマンド実行中に見つかったり、/var/log/messages にエラーメッセージが出力されていたりします。
# dmesg | grep "READ ERROR"

当然ながら、ディスクエラーがない場合には何も出力されない。

便利コマンドその3: アクセスの多いIPアドレスを特定する - netstatコマンド

netstatの標準出力からIPアドレスだけをcutコマンドで抽出し、uniqコマンドで同じ文字列をカウントするようにします。
$ netstat -an|cut -c 45-66|cut -f 1-4 -d.|sort|uniq -c|sort -r
  58          0 ffffff800bb
  50          0 ffffff800b7
  44          0 ffffff800d7
  32 *.*                   
  21 192.0.2.1
   6 192.0.2.2
   2 192.0.2.3
   1 rs)
   1 Foreign Address       
   1 800df87f00            
  

上の例は本に載っているとおりにコマンドを実行したものだが、UNIXドメインソケットの情報や出力のヘッダ行にあたる文字列"Foreign Address"とか"rs)"が混ざって分かりにくい。この点を修正したのが以下のコマンド(と言っても-fオプションとsedのフィルタを加えただけ)。

$ netstat -f inet -an|sed '1,2d'|cut -c 45-66|cut -f 1-4 -d.|sort|uniq -c|sort -r
  21 192.0.2.1
  18 *.*                   
   2 192.0.2.2
   2 192.0.2.3
   1 192.0.2.4
   1 192.0.2.5
   1 192.0.2.6
  

また、環境によってはcutコマンドのオプションすなわち-c 45-66の部分を調整する必要があるかもしれない。